diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a48fbaa..6b045df4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,32 +1,308 @@ -name: CI - -on: - push: - pull_request: - workflow_dispatch: - -jobs: - test: - name: Test / Fmt / Clippy - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Rustfmt - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - - - name: Test - run: cargo test --workspace --all-features --locked +# name: CI +# +# on: +# push: +# pull_request: +# workflow_dispatch: +# +# permissions: +# contents: read +# +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref }} +# cancel-in-progress: true +# +# env: +# CARGO_TERM_COLOR: always +# RUST_BACKTRACE: 1 +# RUST_VERSION: "1.97.0" +# +# jobs: +# fmt: +# name: Rustfmt +# runs-on: ubuntu-latest +# timeout-minutes: 10 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# components: rustfmt +# +# - name: Check repository release invariants +# shell: bash +# run: | +# while read -r action; do +# [[ "$action" == ./* ]] && continue +# revision="${action##*@}" +# if [[ ! "$revision" =~ ^[0-9a-f]{40}$ ]]; then +# echo "GitHub Action is not pinned to a full commit SHA: $action" >&2 +# exit 1 +# fi +# done < <(awk '/^[[:space:]]*uses:/ { print $2 }' .github/workflows/ci.yml) +# if git check-ignore --quiet --no-index crates/example/src/debug/mod.rs; then +# echo "Nested source directory named debug must not be ignored" >&2 +# exit 1 +# fi +# git check-ignore --quiet --no-index .claude/worktrees/example +# if grep --quiet '^serde-wasm-bindgen[[:space:]]*=' crates/lsp/Cargo.toml; then +# echo "solcore-lsp must not expose its removed dead dependency" >&2 +# exit 1 +# fi +# +# - name: Check formatting +# run: cargo fmt --all -- --check +# +# check: +# name: Check (${{ matrix.os }}) +# runs-on: ${{ matrix.os }} +# timeout-minutes: 20 +# strategy: +# fail-fast: false +# matrix: +# os: +# - ubuntu-latest +# - macos-latest +# - windows-latest +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Check workspace +# run: cargo check --workspace --all-targets --all-features --locked +# +# clippy: +# name: Clippy +# runs-on: ubuntu-latest +# timeout-minutes: 20 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# components: clippy +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Lint workspace +# run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings +# +# wasm-build: +# name: Wasm build +# runs-on: ubuntu-latest +# timeout-minutes: 45 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# targets: wasm32-unknown-unknown +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Install Node +# uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# with: +# node-version: "22" +# cache: npm +# cache-dependency-path: | +# playground/package-lock.json +# editors/vscode-solcore/package-lock.json +# +# - name: Install wasm-pack +# run: cargo install wasm-pack --version 0.15.0 --locked +# +# - name: Install playground dependencies +# working-directory: playground +# run: npm ci +# +# - name: Test playground language providers +# working-directory: playground +# run: npm run test:unit +# +# - name: Build release playground +# working-directory: playground +# run: npm run build +# +# - name: Validate VS Code extension package +# working-directory: editors/vscode-solcore +# run: | +# npm ci +# npm run test:unit +# node --check extension.js +# npm pack --dry-run +# +# test: +# name: Test +# runs-on: ubuntu-latest +# timeout-minutes: 30 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Run tests +# run: cargo test --workspace --all-features --locked +# +# platform-test: +# name: Platform tests (${{ matrix.os }}) +# runs-on: ${{ matrix.os }} +# timeout-minutes: 30 +# strategy: +# fail-fast: false +# matrix: +# os: +# - macos-latest +# - windows-latest +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Run path and editor integration tests +# run: >- +# cargo test --all-features --locked +# -p solcore-parser +# -p solcore-nameres +# -p solcore-vfs +# -p solcore-lsp +# -p solcore-driver +# +# performance: +# name: Performance pathology guard +# runs-on: ubuntu-latest +# timeout-minutes: 30 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Run scaling workloads with generous ceilings +# run: scripts/check-compile-performance.sh +# +# e2e: +# name: E2E +# runs-on: ubuntu-latest +# timeout-minutes: 45 +# +# env: +# E2E: "1" +# E2E_REQUIRED: "1" +# ANVIL_HARDFORK: "osaka" +# # Published in https://binaries.soliditylang.org/linux-amd64/list.json. +# SOLC_SHA256: fa8ac9a32d301ad023a36ee5a29f8e291fe3200c60244e43c142539e82a617f4 +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Install Foundry +# uses: foundry-rs/foundry-toolchain@b00af27efadbc7b4ca8b82abbd903b17cc874d2a # v1.9.0 +# with: +# version: v1.7.1 +# +# - name: Configure solc path +# run: echo "SOLC=$RUNNER_TEMP/solc" >> "$GITHUB_ENV" +# +# - name: Install solc +# run: | +# curl --fail --location --retry 3 \ +# --output "$SOLC" \ +# https://github.com/ethereum/solidity/releases/download/v0.8.35/solc-static-linux +# echo "$SOLC_SHA256 $SOLC" | sha256sum --check --strict +# chmod +x "$SOLC" +# +# - name: Show tool versions +# run: | +# "$SOLC" --version +# anvil --version +# cast --version +# +# - name: Compile Yul regression corpus with solc +# run: >- +# cargo test --profile e2e -p solcore-yul --test snapshots --locked +# solc_strict_assembly_compiles_snapshots_and_repros_when_present -- +# --nocapture +# +# - name: Run E2E tests +# run: >- +# cargo test --profile e2e -p solcore-yul -p solcore-sonatina +# --test e2e --locked -- --nocapture --test-threads=1 +# +# docs: +# name: Docs +# runs-on: ubuntu-latest +# timeout-minutes: 20 +# +# env: +# RUSTDOCFLAGS: -D warnings +# +# steps: +# - name: Checkout +# uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 +# +# - name: Install Rust +# uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 +# with: +# toolchain: ${{ env.RUST_VERSION }} +# +# - name: Rust cache +# uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 +# +# - name: Build documentation +# run: cargo doc --workspace --all-features --no-deps --locked diff --git a/.gitignore b/.gitignore index 07283388..00611ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ # Generated by Cargo # will have compiled files and executables -debug -target # These are backup files generated by rustfmt **/*.rs.bk +# Generated by Python migration tooling +__pycache__/ +*.py[cod] + # MSVC Windows builds of rustc generate these, which store debugging information *.pdb @@ -24,3 +26,4 @@ target # Added by cargo /target +/.claude/ diff --git a/Cargo.lock b/Cargo.lock index b25f6acd..91d53625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,52 +19,106 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "annotate-snippets" -version = "0.12.11" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e4850548ff4a25a77ce3bda7241874e17fb702ea551f0cc62a2dbe052f1272" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" dependencies = [ "anstyle", + "memchr", "unicode-width", ] [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] -name = "anyhow" -version = "1.0.101" +name = "ar_archive_writer" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] [[package]] -name = "ar_archive_writer" -version = "0.2.0" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "object", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] -name = "beef" -version = "0.5.2" +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "borsh" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "bytes", + "cfg_aliases", +] [[package]] name = "boxcar" @@ -72,11 +126,29 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" -version = "1.2.49" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -88,6 +160,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chumsky" version = "0.12.0" @@ -104,16 +182,40 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "once_cell", "windows-sys", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "cranelift-bitset" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006fe8776f6d81acb83571f52e7737a54c6dec1ba75e2b7b5a68af15451f88ee" + +[[package]] +name = "cranelift-entity" +version = "0.126.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcca10e8c33eac67a45be4e09d236e274697831ca6bf4c4a927f7570eb8436a8" +dependencies = [ + "cranelift-bitset", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -148,6 +250,61 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "rayon", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "dir-test" version = "0.4.1" @@ -171,20 +328,40 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "dot2" +version = "1.0.0" +source = "git+https://github.com/sanpii/dot2.rs.git#1b4e48757c01b34a54952e4c6a7c541290a92c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "ena" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] [[package]] name = "encode_unicode" @@ -210,15 +387,24 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "static_assertions", +] [[package]] name = "fnv" @@ -232,6 +418,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -241,17 +433,103 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" -version = "0.4.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", - "wasip3", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", ] [[package]] @@ -260,6 +538,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[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.5" @@ -268,38 +552,50 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.17.1", ] [[package]] -name = "heck" -version = "0.5.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -307,9 +603,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -320,9 +616,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -334,15 +630,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -354,15 +650,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -373,12 +669,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -392,9 +682,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -402,21 +692,28 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown 0.17.1", +] + +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", ] [[package]] name = "insta" -version = "1.46.3" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -426,51 +723,72 @@ dependencies = [ [[package]] name = "intrusive-collections" -version = "0.9.7" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +checksum = "4b719c59241cfaac1042a6d26787e28ed7ee4a4e21a5a907786f54222d1b0062" dependencies = [ "memoffset", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "line-index" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" +dependencies = [ + "nohash-hasher", + "text-size", +] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -489,43 +807,69 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a790d11254054e5dc83902dba85d253ff06ceb0cfafb12be8773435cb9dfb4f4" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60337c43a38313b58871f8d5d76872b8e17aa9d51fad494b5e76092c0ce05f5" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" dependencies = [ - "beef", "fnv", "proc-macro2", "quote", - "regex-automata 0.4.13", - "regex-syntax 0.8.8", - "rustc_version", + "regex-automata 0.4.14", + "regex-syntax 0.8.11", "syn", ] [[package]] name = "logos-derive" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d151b2ae667f69e10b8738f5cac0c746faa22b2e15ea7e83b55476afec3767dc" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" dependencies = [ "logos-codegen", ] +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "matchcov" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40459aafa8d5c25a61c1e176a86d8111b508927716c76f98e4f66a007eb6ee86" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata 0.4.14", +] + [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" @@ -537,22 +881,65 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.32.2" +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "memchr", + "windows-sys", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] [[package]] -name = "parking_lot" +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" @@ -580,11 +967,73 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +dependencies = [ + "pest", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" @@ -594,37 +1043,61 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "syn", + "zerocopy", +] + +[[package]] +name = "primitive-types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721a1da530b5a2633218dc9f75713394c983c352be88d2d7c9ee85e2c4c21794" +dependencies = [ + "fixed-hash", + "uint", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.0", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.11", + "unarray", +] + [[package]] name = "psm" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -632,9 +1105,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -645,11 +1118,55 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -671,7 +1188,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -687,13 +1204,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.8", + "regex-syntax 0.8.11", ] [[package]] @@ -704,32 +1221,23 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -744,14 +1252,14 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "salsa" -version = "0.25.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e2aa2fca57727371eeafc975acc8e6f4c52f8166a78035543f6ee1c74c2dcc" +checksum = "ffbaab832e2ea754afda4a738f987dd1e8bd30c9e5d8c981ee6a3934386095e2" dependencies = [ "boxcar", "crossbeam-queue", "crossbeam-utils", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "hashlink", "indexmap", "intrusive-collections", @@ -765,19 +1273,20 @@ dependencies = [ "smallvec", "thin-vec", "tracing", + "typeid", ] [[package]] name = "salsa-macro-rules" -version = "0.25.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfc2a1e7bf06964105515451d728f2422dedc3a112383324a00b191a5c397a3" +checksum = "de6872462ac73d39969a836273c24163e6a26a4e08f5114fcd80e25af30ea9c6" [[package]] name = "salsa-macros" -version = "0.25.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d844c1aa34946da46af683b5c27ec1088a3d9d84a2b837a108223fd830220e1" +checksum = "76bc78ffaf65b1a9175818592c5130aa10b1bb245a905722fd4db87cea8a8457" dependencies = [ "proc-macro2", "quote", @@ -791,12 +1300,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" version = "1.0.228" @@ -807,6 +1310,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -829,9 +1343,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -840,11 +1354,31 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "similar" @@ -852,19 +1386,65 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smol_str" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "solcore-compiler" +version = "0.1.0" +dependencies = [ + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-hull", + "solcore-nameres", + "solcore-parser", + "solcore-specialize", + "solcore-test-utils", + "tracing", + "url", +] [[package]] name = "solcore-driver" version = "0.1.0" dependencies = [ + "annotate-snippets", + "libc", + "rustc-hash", "salsa", + "serde_json", + "solcore-compiler", "solcore-hir", + "solcore-hir-ty", + "solcore-hull", + "solcore-nameres", "solcore-parser", + "solcore-sonatina", + "solcore-specialize", + "solcore-vfs", + "solcore-yul", + "tracing", + "tracing-subscriber", "url", ] @@ -873,384 +1453,764 @@ name = "solcore-hir" version = "0.1.0" dependencies = [ "annotate-snippets", + "percent-encoding", + "rustc-hash", "salsa", + "tracing", "url", ] [[package]] -name = "solcore-parser" +name = "solcore-hir-ty" version = "0.1.0" dependencies = [ - "annotate-snippets", - "chumsky", "dir-test", - "insta", - "logos", + "ena", + "matchcov", + "num-bigint", + "proptest", + "rustc-hash", "salsa", "solcore-hir", + "solcore-nameres", + "solcore-parser", + "solcore-test-utils", + "tablesolve", + "tracing", + "url", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" +name = "solcore-hull" +version = "0.1.0" dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", + "rustc-hash", + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-nameres", + "solcore-parser", + "solcore-specialize", + "url", ] [[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +name = "solcore-lsp" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "line-index", + "lsp-types", + "percent-encoding", + "serde", + "serde_json", + "solcore-hir", + "solcore-hir-ty", + "solcore-nameres", + "solcore-parser", + "solcore-vfs", + "stacker", + "tokio", + "tower-lsp", + "url", + "wasm-bindgen", ] [[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +name = "solcore-nameres" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "annotate-snippets", + "rustc-hash", + "salsa", + "solcore-hir", + "solcore-parser", + "tracing", + "url", ] [[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +name = "solcore-parser" +version = "0.1.0" dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", + "annotate-snippets", + "chumsky", + "dir-test", + "insta", + "logos", + "proptest", + "salsa", + "solcore-hir", + "tracing", ] [[package]] -name = "thin-vec" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +name = "solcore-sonatina" +version = "0.1.0" dependencies = [ - "displaydoc", - "zerovec", + "dir-test", + "insta", + "salsa", + "smallvec", + "solcore-hir", + "solcore-hir-ty", + "solcore-hull", + "solcore-nameres", + "solcore-parser", + "solcore-specialize", + "solcore-test-utils", + "sonatina-codegen", + "sonatina-ir", + "sonatina-triple", + "sonatina-verifier", ] [[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +name = "solcore-specialize" +version = "0.1.0" dependencies = [ - "pin-project-lite", - "tracing-core", + "rustc-hash", + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-nameres", + "solcore-parser", + "url", ] [[package]] -name = "tracing-core" -version = "0.1.36" +name = "solcore-test-utils" +version = "0.1.0" +dependencies = [ + "annotate-snippets", + "insta", + "rustc-hash", + "salsa", + "serde_json", + "solcore-hir", + "solcore-nameres", + "solcore-parser", + "url", +] + +[[package]] +name = "solcore-uitest" +version = "0.1.0" +dependencies = [ + "dir-test", + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-hull", + "solcore-nameres", + "solcore-parser", + "solcore-specialize", + "solcore-test-utils", +] + +[[package]] +name = "solcore-vfs" +version = "0.1.0" +dependencies = [ + "rustc-hash", + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-nameres", + "solcore-parser", + "url", +] + +[[package]] +name = "solcore-wasm" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "serde", + "serde-wasm-bindgen", + "serde_json", + "solcore-compiler", + "solcore-hull", + "solcore-nameres", + "solcore-sonatina", + "solcore-vfs", + "solcore-yul", + "wasm-bindgen", +] + +[[package]] +name = "solcore-yul" +version = "0.1.0" +dependencies = [ + "dir-test", + "insta", + "rustc-hash", + "salsa", + "solcore-hir", + "solcore-hir-ty", + "solcore-hull", + "solcore-nameres", + "solcore-parser", + "solcore-specialize", + "solcore-test-utils", + "url", +] + +[[package]] +name = "sonatina-codegen" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "bit-set", + "cranelift-entity", + "dashmap 6.2.1", + "indexmap", + "rayon", + "rustc-hash", + "smallvec", + "sonatina-ir", + "sonatina-macros", + "sonatina-triple", + "sonatina-verifier", + "tracing", + "vec-collections", +] + +[[package]] +name = "sonatina-ir" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "bit-set", + "bitflags 2.13.0", + "cranelift-entity", + "dashmap 6.2.1", + "dot2", + "dyn-clone", + "indexmap", + "parking_lot", + "primitive-types", + "rayon", + "rustc-hash", + "smallvec", + "smol_str", + "sonatina-macros", + "sonatina-triple", + "vec-collections", +] + +[[package]] +name = "sonatina-macros" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sonatina-parser" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "annotate-snippets", + "bimap", + "cranelift-entity", + "derive_more", + "either", + "hex", + "pest", + "pest_derive", + "rustc-hash", + "smallvec", + "smol_str", + "sonatina-ir", + "sonatina-triple", + "tracing", +] + +[[package]] +name = "sonatina-triple" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "thiserror", +] + +[[package]] +name = "sonatina-verifier" +version = "0.0.3-alpha" +source = "git+https://github.com/fe-lang/sonatina?rev=55ca888#55ca888f1fc83077e5eee803c0619231e9b50998" +dependencies = [ + "cranelift-entity", + "rayon", + "rustc-hash", + "smallvec", + "sonatina-ir", + "sonatina-macros", + "sonatina-parser", + "sonatina-triple", + "tracing", +] + +[[package]] +name = "sorted-iter" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "bceb57dc07c92cdae60f5b27b3fa92ecaaa42fe36c55e22dbfb0b44893e0b1f7" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tablesolve" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49838cdd952e69beb44e8ecf05aa29c5e80a7d4a15f12252daecb24998bce43c" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", "once_cell", + "rustix", + "windows-sys", ] [[package]] -name = "unicode-ident" -version = "1.0.22" +name = "text-size" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "thin-vec" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" [[package]] -name = "unicode-width" -version = "0.2.2" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "url" -version = "2.5.8" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "cfg-if", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" +name = "tokio" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "wit-bindgen", + "bytes", + "pin-project-lite", + "tokio-macros", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ - "wit-bindgen", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "leb128fmt", - "wasmparser", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "tower" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "async-trait", + "auto_impl", + "bytes", + "dashmap 5.5.3", + "futures", + "httparse", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tower-lsp-macros", + "tracing", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "tower-lsp-macros" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows-sys" -version = "0.59.0" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "windows-targets", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 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", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata 0.4.14", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "typeid" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "ucd-trie" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "uint" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "unarray" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec-collections" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9965c8f2ffed1dbcd16cafe18a009642f540fa22661c6cfd6309ddb02e4982" dependencies = [ - "wit-bindgen-rust-macro", + "binary-merge", + "inplace-vec-builder", + "lazy_static", + "num-traits", + "smallvec", + "sorted-iter", ] [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "anyhow", - "heck", - "wit-parser", + "wit-bindgen", ] [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "wasm-bindgen" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "wasm-bindgen-macro" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ - "anyhow", - "prettyplease", + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wit-bindgen-core", - "wit-bindgen-rust", + "wasm-bindgen-shared", ] [[package]] -name = "wit-component" -version = "0.244.0" +name = "wasm-bindgen-shared" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "unicode-ident", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", + "windows-link", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1259,9 +2219,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1269,20 +2229,40 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1292,9 +2272,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1303,9 +2283,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1314,9 +2294,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -1325,6 +2305,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5dfea8f8..8260a705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,58 @@ members = ["crates/*"] resolver = "3" [workspace.dependencies] -salsa = "0.25" +salsa = "0.27" url = "2.5" annotate-snippets = "0.12" +rustc-hash = "2" +percent-encoding = "2.3" +ena = "0.14" +matchcov = "0.1.0" +tablesolve = "0.1.0" +num-bigint = "0.4.6" +proptest = { version = "1.11.0", default-features = false, features = ["std"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +smallvec = "1" +sonatina-codegen = { git = "https://github.com/fe-lang/sonatina", rev = "55ca888" } +sonatina-ir = { git = "https://github.com/fe-lang/sonatina", rev = "55ca888" } +sonatina-triple = { git = "https://github.com/fe-lang/sonatina", rev = "55ca888" } +sonatina-verifier = { git = "https://github.com/fe-lang/sonatina", rev = "55ca888" } parser = { path = "crates/parser", package = "solcore-parser" } hir = { path = "crates/hir", package = "solcore-hir" } +nameres = { path = "crates/nameres", package = "solcore-nameres" } +hir-ty = { path = "crates/hir-ty", package = "solcore-hir-ty" } +compiler = { path = "crates/compiler", package = "solcore-compiler" } +vfs = { path = "crates/vfs", package = "solcore-vfs" } [workspace.package] edition = "2024" +rust-version = "1.97.0" + +# Native release builds favor compiler throughput. Browser bundles use the +# separate `wasm-release` profile below so native and wasm builds do not trade +# off against one another. +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 16 +strip = true + +# Keep the browser bundle on the previous size-oriented settings. Playground +# wasm-pack commands select this profile explicitly. +[profile.wasm-release] +inherits = "release" +opt-level = "z" +lto = true +codegen-units = 1 +strip = true + +# E2E execution spends most of its time building test binaries and external +# EVM artifacts. Moderate optimization is sufficient and avoids the release +# profile's LTO cost. +[profile.e2e] +inherits = "release" +opt-level = 2 +lto = false +codegen-units = 16 +strip = true diff --git a/SEMANTIC_DIFFERENCES.md b/SEMANTIC_DIFFERENCES.md new file mode 100644 index 00000000..cee44d67 --- /dev/null +++ b/SEMANTIC_DIFFERENCES.md @@ -0,0 +1,222 @@ +# Haskell/Rust semantic compatibility and standard-library policy + +This document is the canonical record of known semantic differences between +the Haskell and Rust Solcore implementations. It records which behavior should +win and where a fix belongs. The parity TSV files are executable test ledgers; +they are not the language specification. + +## Comparison baseline + +- Haskell reference: [`argotorg/solcore@ac6f8957`](https://github.com/argotorg/solcore/tree/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f). +- Rust implementation: `solcore-rs@631d40814b28755bfd0afb6fa97a7a79895fa6ee`. +- Standard library: the byte-identical `ac6f8957` snapshot in [`std/`](std/). +- Validation date: 2026-07-14. + +The reference corpus in +[`reference-frontend.tsv`](crates/parser/tests/fixtures/corpus/reference-frontend.tsv) +was produced with Haskell flags `-n -g`: specialization/Hull emission and +generated contract dispatch were disabled. It used the default legacy +trait resolver. Its 278 passes, 153 failures, and two timeouts describe +that configuration, not the whole Haskell compiler. + +The Rust accepted-corpus gate in +[`frontend_smoke.rs`](crates/hir-ty/tests/frontend_smoke.rs) runs the full Rust +frontend, including generated dispatch. Keep these categories separate: + +1. a genuine language/type-system difference; +2. a solver-mode difference (`legacy` versus `tabled`); +3. a phase difference (`-g` versus generated dispatch); +4. a shared-std defect; and +5. an implementation defect after both sides run in the same mode. + +Written syntax and safety invariants take priority over accidentally accepted +legacy fixtures. For an external ABI, a type is supported only when ABI +metadata, selector spelling, argument decoding, and result encoding agree. + +## Decision summary + +“Owner” identifies the implementation that should change. “Harness” means that +the compiler behaviors already agree once the same options are used. + +| Area / witness | Observed behavior | Recommendation and owner | +| --- | --- | --- | +| `for` post-clause `let` ([fixture](crates/parser/tests/fixtures/corpus/fail/test/examples/cases/for-let-post.solc)) | Rust accepts it; the Haskell parser accepts only assignments in the post clause. | A post clause has the same forms as an init clause, as the Haskell language documentation says. **Fix Haskell parser and its negative fixture.** | +| Calling a `word` (`Uncurry`, `rec`) | Haskell accepts invocation of a value annotated as `word`; Rust reports a non-callable value. | Only function/invokable values are callable. **Fix Haskell type checking; keep Rust.** | +| Explicit closure desugaring ([fixture](crates/parser/tests/fixtures/corpus/fail/test/examples/cases/compose_desugared.solc)) | Haskell says the generated-style `invoke` implementation is not polymorphic enough; Rust accepts it. | Accept the explicit representation if it is valid closure-conversion output. **Fix Haskell rank-polymorphic checking**, while retaining a Rust specialization regression. | +| Narrowed impl member ([fixture](crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ixa.solc)) | Haskell accepts `function size(value: Proxy>) returns (word)` where the instantiated trait requires `Proxy>>`; Rust rejects it. | An impl member must implement the instantiated trait signature. **Fix Haskell impl checking; keep Rust.** | +| Recursive/table-reuse fixtures | Haskell legacy rejects `super-class-recursive-arg`, `tabled-answer-reuse`, and `tabled-mutual-chain`; Haskell tabled mode and Rust accept them. | These are not semantic differences under the tabled resolver. Make tabled canonical, or record the mode in each verdict. **Fix Haskell configuration and the harness.** | +| Polymorphic comptime argument ([fixture](crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_poly_runtime.solc)) | The Haskell legacy frontend first reports ambiguity. Both Haskell tabled full-pipeline mode and Rust specialization reject a runtime value passed to a comptime parameter; the Rust frontend-only parity probe intentionally defers it. | The latent comptime obligation is already preserved through Rust specialization. **Keep the specialization regression and record the phase in the harness.** | +| Parameterized contract `main` ([fixture](crates/parser/tests/fixtures/corpus/ok/test/examples/cases/multi-stmt-var-leaf.solc)) | Haskell suppresses generated dispatch whenever a local `main` exists and accepts parameters; Rust rejects them because the runtime entry receives no arguments. | A source runtime entry must be zero-argument. **Fix Haskell dispatch validation; keep Rust.** | +| Missing helper imports (`field-helper-cxt-collision`, `pair-bug`) | Haskell `-g` verdicts pass; both full frontends fail because the fixtures omit `std.dispatch`. | This is a mode mismatch. Compare both with dispatch or both without it. **Fix the harness/fixtures.** | +| Primitive `word` in public ABI | Both metadata emitters call it `uint256`, but shared std cannot dispatch source `word`. Both compilers report missing evidence; current Rust tabled resolution terminates with a bounded `SC0207`. | Add complete `word` evidence in the **upstream Haskell std**, then re-vendor. Keep a Rust regression proving bounded failure while evidence is missing. | +| User ADTs in public ABI | Haskell metadata passes through a nullary source name or crashes on a parameterized ADT; runtime `SigString` is absent. Rust rejects user ADTs from the canonical external ABI with a structured diagnostic. | Keep Rust's rejection. Design layout/spelling/codec semantics in the **upstream language and std**, then fix the Haskell surface before implementing support in both compilers. | +| ABI type validation | Haskell passes other nullary names through and uses `error` for unsupported shapes. Rust uses canonical checks and diagnostics. | Validate against the dispatchable ABI surface. **Fix the Haskell ABI emitter; keep Rust's diagnostic model.** | +| Signature/selector collisions | Rust rejects duplicate signatures and distinct signatures with the same four-byte selector. Haskell has no equivalent preflight. | Reject both before code generation. **Fix Haskell dispatch generation.** | +| Nested tuple boundary | Both flatten the language's right-nested pair representation at the top ABI boundary. | This is shared. **Fix both compilers and the language ABI design together** if nested boundaries must be preserved. | + +## Evidence and rationale + +### Syntax and ordinary type checking + +Haskell +[`forPostP`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/src/Solcore/Frontend/Parser/Stmt.hs#L113-L120) +uses only `forAssignP`, while its init parser also uses `forLetP`. The same +revision's [syntax documentation](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/doc/src/sail/syntax.md#L333-L339) +says the post clause follows the init grammar. Rust's +[`parsed_stmt_parser`](crates/parser/src/parse/stmt.rs) uses one `for_item` for +both positions. Haskell is the outlier. + +Haskell's acceptance of calls through a `word` annotation remains a +type-checking defect. The `ixa` case is another Haskell false acceptance: +substitution of the impl head into the trait signature does not equal the +implementation signature. Rust's `SC0221` should remain. + +### Resolver and comptime modes + +Haskell defaults to `LegacyResolution` in +[`Options.hs`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/src/Solcore/Pipeline/Options.hs#L55-L72), +while its tabled tests select `TabledResolution` in +[`test/Cases.hs`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/test/Cases.hs#L628-L657). +Direct runs confirm that the three recursive/reuse fixtures pass in tabled +mode. Their legacy failures must not be described as Rust solver extensions. + +`ct_param_poly_runtime.solc` is a phase-sensitive case rather than a remaining +semantic difference. Haskell tabled full-pipeline mode and Rust specialization +both report a runtime value passed to `Wrap.unwrap`'s comptime parameter, while +the Rust frontend-only corpus probe has not reached that phase. Haskell also +intentionally defers polymorphic cases in +[`Frontend/ComptimeCheck.hs`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/src/Solcore/Frontend/ComptimeCheck.hs#L196-L215). +Rust already has latent-call analysis in +[`infer/comptime.rs`](crates/hir-ty/src/infer/comptime.rs) and specialization +checks in [`evaluate/core.rs`](crates/specialize/src/evaluate/core.rs); the +obligation survives into that check and produces `SC0409`. + +### Contract lowering and parity configuration + +Haskell +[`contractDispatchTopDecls`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/src/Solcore/Desugarer/ContractDispatch.hs#L36-L43) +suppresses generated runtime dispatch for any contract-local `main`, without an +arity check. Rust mirrors suppression but adds +[`contract_runtime_main_diagnostics`](crates/hir-ty/src/contract/dispatch.rs), +because the runtime convention invokes it with no arguments. Haskell should add +the same validation. + +Ordinary Haskell corpus tests and the verdict generator disable dispatch. Rust +`reference_accepted_corpus_passes_the_full_frontend` enables it. Thus +`field-helper-cxt-collision` and `pair-bug` pass only the Haskell no-dispatch +run; a Haskell full run rejects the same missing `std.dispatch` names. They do +not demonstrate implicit Haskell bindings. + +The current +[`rust-rejected-reference-passes.tsv`](crates/parser/tests/fixtures/corpus/rust-rejected-reference-passes.tsv) +has 67 diagnostic rows across 45 paths. Thirty-nine paths are intentional Rust +negative `imports/*` fixtures; only six unique non-import paths remain as +reference compatibility cases. Manifest row counts are not +semantic-difference counts. + +### ABI metadata, selectors, and runtime evidence + +Haskell +[`abiTypeOf`](https://github.com/argotorg/solcore/blob/ac6f8957a78dc53248dbe053f1ddbc2a2201b81f/src/Solcore/Desugarer/ContractDispatch.hs#L385-L398) +and Rust [`abi_type_of`](crates/hir-ty/src/contract/abi.rs) render primitive +`word` as `uint256`. Runtime dispatch is separate: generated `Method` values +retain source types and require classes from `std.dispatch` and `std`. + +The current shared snapshot has this evidence matrix: + +| Source ABI type | Selector spelling (inputs) | Decode input | Encode result | Status | +| --- | --- | --- | --- | --- | +| `uint256` | yes | yes | yes | complete | +| `address` | yes | yes | yes | complete | +| `bytes32` | yes | yes | yes | complete | +| `memory(string)` | yes | yes | yes | complete | +| `memory(bytes)` | yes | yes | yes | complete | +| `()` | yes | yes | yes | complete | +| `bool` | **no** | **no** | yes | output-only | +| `word` (ABI `uint256`) | **no** | **no** | **no** | unsupported by dispatch | +| pair/tuple | recursive | recursive | recursive | complete only when all components are complete | +| user ADT | **no generic impl** | representation helpers only | representation helpers only | no complete external contract | + +For `word`, the minimum upstream std correction is: + +- `word : SigString`, returning `"uint256"`; +- `word : ABIEncode`, storing the primitive word directly; and +- `ABIDecoder(word, reader) : ABIDecode(word)`, reading it directly. + +Until that upstream change is re-vendored, Rust's table-entry and work-fuel +bounds make the generated dispatch probe terminate with `SC0207`; the +`missing_word_abi_evidence` UI regression exercises the real shared std path. + +The same audit should add input-side `bool` evidence if boolean parameters are +intended to be public. A result-only bool works because selectors omit result +types and `bool : ABIEncode` exists; that does not make bool a supported input. + +Shared `std/ABIGeneric.solc` provides representation helpers, but +`std/dispatch.solc` has no generic canonical `SigString`. Haskell emits an +arbitrary nullary user type name as if it were a Solidity ABI name and reaches +a partial `error` for parameterized user types; Rust rejects both forms. A +stable external ADT design is required before either compiler can support this +surface completely. + +Both emitters flatten right-nested pairs. Haskell does so in `flattenTuple` and +Rust in [`flatten_tuple`](crates/hir-ty/src/contract/abi.rs); observable behavior +is recorded in [`tests/e2e/README.md`](tests/e2e/README.md). The source +representation has already erased some nested boundaries, so std alone cannot +fix this. + +Rust checks duplicate signatures and four-byte selector collisions in +[`contract/dispatch.rs`](crates/hir-ty/src/contract/dispatch.rs). Haskell should +perform the same preflight, replace partial ABI-renderer errors with structured +diagnostics, and replace arbitrary nullary-name passthrough with a canonical +allowlist or evidence-based query. + +## Standard-library recommendation + +The `.solc` files in [`std/`](std/) are a shared compatibility artifact, not a +Rust fork. Do not apply Rust-only semantic edits. A shared-std fix must: + +1. reproduce with the pinned Haskell compiler and upstream std; +2. be fixed and tested in upstream Haskell std first; +3. pin the new upstream revision; +4. be re-vendored byte-for-byte into `std/` and + `crates/parser/tests/fixtures/corpus/ok/std/`; and +5. pass full dispatch and backend tests on both implementations. + +The required invariant is: + +> A public source type is supported if and only if ABI JSON can represent it, +> its canonical input signature can be hashed, calldata can be decoded into it, +> and a result can be encoded from it. + +The next upstream std change should complete `word` and input-side `bool`, then +add argument/result matrix tests for `word`, `uint256`, `address`, `bytes32`, +`bool`, `memory(string)`, `memory(bytes)`, and supported tuples. Unsupported +location wrappers, std leaf types, and user ADTs must be explicitly rejected +until specified. Each test must use generated selector dispatch and must not +define source `main`, because source `main` suppresses the path under test. + +Haskell ABI diagnostics and collision checks do not belong in std. Keep `import +std.dispatch.{*};` explicit until both compilers have a specified +compiler-private dependency mechanism. + +## Keeping the parity ledger honest + +- Record Haskell solver mode and enabled phases with every generated verdict. +- Compare the same pipeline on both sides: full dispatch for contract fixtures, + and no dispatch for isolated frontend fixtures. +- Exclude intentional Rust-only negative import fixtures from reference-pass + difference counts. +- When resolving a divergence, remove its TSV allowance and add the smallest + regression on the corrected side. + +After a std update, verify the copies and then the full pipelines: + +```sh +for file in ABIGeneric.solc Generic.solc dispatch.solc opcodes.solc std.solc; do + cmp "std/$file" "crates/parser/tests/fixtures/corpus/ok/std/$file" || exit 1 +done +cargo test -p solcore-parser -p solcore-hir-ty -p solcore-specialize --locked +E2E=1 E2E_REQUIRED=1 cargo test --profile e2e \ + -p solcore-yul -p solcore-sonatina --test e2e --locked -- \ + --nocapture --test-threads=1 +``` diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..5d174840 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,73 @@ +# Compiler wall-time benchmark + +`scripts/bench-compile.sh` measures cold compiler sessions through the native +`solcore-driver` process. It builds the driver once, then uses `hyperfine` with +shell execution disabled. Every timed invocation creates a fresh compiler +database; filesystem pages and the executable are warmed by the configured +warmup runs. + +The fixed cases cover distinct compiler workloads: + +| Case | Fixture | Purpose | +| --- | --- | --- | +| `std-free` | `SingleFun.solc` | Small frontend run without reachable std/runtime | +| `dispatch-small` | `tests/e2e/022add` | Small contract with compiler-owned dispatch | +| `erc20-large` | `tests/e2e/128minierc20` | Larger std- and storage-heavy contract | +| `multi-file` | `tests/e2e/ltimp` | Main module plus a local import | + +Each case is measured at five end-to-end boundaries: + +- `frontend`: parse, resolve, diagnostics, and type checking +- `hull`: frontend, specialization, Hull emission/checking, and Hull rendering +- `yul`: frontend and the shared Hull pipeline followed by Yul rendering +- `sonatina`: frontend and the shared Hull pipeline followed by Sonatina IR rendering +- `all`: the shared pipeline followed by all three textual outputs + +The backend stages intentionally write their artifacts to a temporary directory +so terminal rendering is outside the measurement. The driver currently does not +expose specialization as a standalone CLI boundary; `hull` is the first fixed +measurement that includes it. + +Install `hyperfine`, quiet other CPU-heavy work, and run from anywhere inside +the checkout: + +```sh +./scripts/bench-compile.sh +``` + +Defaults are two warmups and ten measured runs per command. Environment +variables make focused development runs possible without changing the suite: + +```sh +BENCH_RUNS=30 BENCH_WARMUP=5 \ +BENCH_EXPORT_DIR=target/bench-results \ +./scripts/bench-compile.sh + +BENCH_STAGE=frontend BENCH_CASE=dispatch-small \ +./scripts/bench-compile.sh +``` + +`BENCH_STAGE` accepts `frontend`, `hull`, `yul`, `sonatina`, or `all`; +`BENCH_CASE` accepts one of the four case names above. Set +`BENCH_SKIP_BUILD=1` to reuse an already-built binary. `BENCH_PROFILE` defaults +to the native speed-oriented `release` profile. If Cargo is configured with a +target triple or a nonstandard output layout, point `SOLCORE_DRIVER` at the +binary explicitly. + +For comparable before/after results, record `rustc --version`, the git commit, +machine/power state, and the JSON files produced through `BENCH_EXPORT_DIR`. + +## CI pathology guard + +The manual Hyperfine suite above is intended for before/after measurement, not +for a stable threshold across different machines. CI separately runs +`scripts/check-compile-performance.sh`, which is a coarse pathology detector. +It compiles generated 2,000-binding and 500-impl modules through Hull and +re-analyzes a 256-function VFS module across 64 edits. Each case has an +independent, deliberately generous 45-second ceiling, configurable through +`PERF_CASE_TIMEOUT_SECONDS` for slower development machines. + +The guard does not treat small timing fluctuations as regressions. Its purpose +is to catch hangs and order-of-magnitude scaling regressions on the large-body, +trait-environment, and incremental editor paths that the cold microbenchmarks +do not exercise. diff --git a/benchmarks/tofu/README.md b/benchmarks/tofu/README.md new file mode 100644 index 00000000..1238b94d --- /dev/null +++ b/benchmarks/tofu/README.md @@ -0,0 +1,13 @@ +# tofu `solc-bench` suite + +`materialize.py` creates a self-contained Standard JSON suite for the shared +Solcore syntax intersection. It deliberately measures compilation through +checked Hull generation, rather than Solidity bytecode size or gas metrics. + +```sh +python3 benchmarks/tofu/materialize.py /data/$USER/solcore-bench +``` + +Use the generated directory with tofu's `bench.sbatch` and either the +`solcore-rs` `--standard-json` binary or tofu's `solcore-standard-json` Haskell +adapter. See the repository-level integration plan for the exact commands. diff --git a/benchmarks/tofu/benchmarks.toml b/benchmarks/tofu/benchmarks.toml new file mode 100644 index 00000000..cd5c6977 --- /dev/null +++ b/benchmarks/tofu/benchmarks.toml @@ -0,0 +1,19 @@ +# The key must match .json generated by materialize.py. +# `evmasm` is the least Solidity-specific solc-bench pipeline selector; both +# adapters ignore the Solidity optimizer/viaIR settings it injects. + +["std-free"] +pipelines = ["evmasm"] +tags = ["fast", "frontend"] + +["dispatch-small"] +pipelines = ["evmasm"] +tags = ["dispatch"] + +["erc20-large"] +pipelines = ["evmasm"] +tags = ["large"] + +["multi-file"] +pipelines = ["evmasm"] +tags = ["imports"] diff --git a/benchmarks/tofu/materialize.py b/benchmarks/tofu/materialize.py new file mode 100644 index 00000000..0e16b18d --- /dev/null +++ b/benchmarks/tofu/materialize.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Generate the shared Solcore Standard JSON suite for tofu's solc-bench.""" + +import argparse +import json +from pathlib import Path +import shutil + + +HERE = Path(__file__).resolve().parent +REPOSITORY = HERE.parents[1] +CASES = { + "std-free": { + "main.solc": REPOSITORY + / "crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SingleFun.solc", + }, + "dispatch-small": { + "main.solc": REPOSITORY / "tests/e2e/022add/main.solc", + }, + "erc20-large": { + "main.solc": REPOSITORY / "tests/e2e/128minierc20/main.solc", + }, + "multi-file": { + "main.solc": REPOSITORY / "tests/e2e/ltimp/main.solc", + "ltproxy.solc": REPOSITORY / "tests/e2e/ltimp/ltproxy.solc", + }, +} + + +def standard_json(sources): + return { + "language": "Solcore", + "sources": { + name: {"content": path.read_text(encoding="utf-8")} + for name, path in sources.items() + }, + "settings": { + "solcore": { + "entrypoint": "main.solc", + "stage": "hull", + }, + "outputSelection": {"*": {"*": []}}, + }, + } + + +def main(): + parser = argparse.ArgumentParser( + description="generate a Solcore suite consumable by tofu's solc-bench" + ) + parser.add_argument("output_dir", type=Path) + arguments = parser.parse_args() + + arguments.output_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(HERE / "benchmarks.toml", arguments.output_dir / "benchmarks.toml") + for name, sources in CASES.items(): + with (arguments.output_dir / f"{name}.json").open("w", encoding="utf-8") as output: + json.dump(standard_json(sources), output, indent=2, sort_keys=True) + output.write("\n") + + +if __name__ == "__main__": + main() diff --git a/crates/compiler/Cargo.toml b/crates/compiler/Cargo.toml new file mode 100644 index 00000000..439954d0 --- /dev/null +++ b/crates/compiler/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "solcore-compiler" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +hir = { workspace = true } +hir-ty = { workspace = true } +hull = { path = "../hull", package = "solcore-hull" } +nameres = { workspace = true } +parser = { workspace = true } +specialize = { path = "../specialize", package = "solcore-specialize" } +tracing = { workspace = true } + +[dev-dependencies] +salsa = { workspace = true } +solcore-test-utils = { path = "../test-utils" } +url = { workspace = true } diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs new file mode 100644 index 00000000..c0b086be --- /dev/null +++ b/crates/compiler/src/lib.rs @@ -0,0 +1,530 @@ +//! Transport-independent orchestration for the Solcore compiler pipeline. +//! +//! This crate owns the stage ordering shared by command-line and browser +//! adapters. It deliberately leaves source loading, output I/O, serialization, +//! and backend text rendering to those adapters. + +use std::{collections::BTreeMap, error::Error, fmt}; + +use hir::{ + ast::item::Item, + diag::{Diagnostic, DiagnosticLevel, sort_dedup_rendered_diagnostics}, + input::SourceFile, +}; +use nameres::{LibraryId, ModuleId, ModuleKey}; + +pub use hir_ty::collect_frontend_diagnostics; + +/// A Hull program that passed specialization, emission, and Hull validation. +/// +/// Non-error diagnostics produced by any completed stage are retained so an +/// adapter can publish warnings without suppressing the requested artifact. +#[derive(Debug)] +pub struct CheckedHull<'db> { + /// Validated Hull program. + pub program: hull::Program<'db>, + /// Warning, note, and help diagnostics accumulated across backend stages. + pub diagnostics: Vec, +} + +/// Specializes `entry_file`, emits Hull, and validates the emitted program. +/// +/// Every stage contributes its diagnostics to one accumulator. A stage that +/// emits an error terminates the pipeline at that boundary and returns all +/// diagnostics accumulated so far; warnings, notes, and help continue to the +/// next stage. Both success and failure diagnostics are sorted and deduplicated. +/// +/// Callers must load the entry and all reachable source modules and reject +/// frontend errors before invoking this backend pipeline. +#[tracing::instrument( + target = "compiler::pipeline", + level = "debug", + skip_all, + fields( + file = %source_file_name(db, entry_file), + max_instantiations = options.max_instantiations, + max_depth = options.max_depth, + max_type_nodes = options.max_type_nodes, + eval_fuel = options.eval_fuel, + ) +)] +pub fn build_checked_hull<'db>( + db: &'db dyn hir_ty::Db, + entry_file: SourceFile, + options: specialize::SpecializeOptions, +) -> Result, Vec> { + let module = parser::parse_file_to_hir(db, entry_file).module(db); + let specialized = tracing::debug_span!(target: "compiler::pipeline", "specialize") + .in_scope(|| specialize::specialize_module(db, module, options)); + let mut diagnostics = Vec::new(); + let stage_start = diagnostics.len(); + let has_errors = append_stage_diagnostics( + &mut diagnostics, + specialized + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)), + ); + tracing::debug!( + target: "compiler::pipeline", + stage = "specialize", + mono_items = specialized.module.items.len(), + entry_points = specialized.module.entry_points.len(), + diagnostics = diagnostics.len() - stage_start, + has_errors, + "compiler stage completed" + ); + if has_errors { + normalize_backend_diagnostics(db, &mut diagnostics); + return Err(diagnostics); + } + + let emitted = tracing::debug_span!(target: "compiler::pipeline", "hull_emit") + .in_scope(|| hull::emit_module(db, &specialized.module, hull::EmitOptions::default())); + let stage_start = diagnostics.len(); + let has_errors = append_stage_diagnostics( + &mut diagnostics, + emitted + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)), + ); + tracing::debug!( + target: "compiler::pipeline", + stage = "hull_emit", + functions = emitted.program.functions.len(), + objects = emitted.program.objects.len(), + diagnostics = diagnostics.len() - stage_start, + has_errors, + "compiler stage completed" + ); + if has_errors { + normalize_backend_diagnostics(db, &mut diagnostics); + return Err(diagnostics); + } + + let checked = tracing::debug_span!(target: "compiler::pipeline", "hull_check") + .in_scope(|| hull::check_program_with_db(db, &emitted.program)); + let stage_start = diagnostics.len(); + let has_errors = append_stage_diagnostics( + &mut diagnostics, + checked.iter().map(|diagnostic| diagnostic.lower(db)), + ); + tracing::debug!( + target: "compiler::pipeline", + stage = "hull_check", + diagnostics = diagnostics.len() - stage_start, + has_errors, + "compiler stage completed" + ); + if has_errors { + normalize_backend_diagnostics(db, &mut diagnostics); + return Err(diagnostics); + } + + normalize_backend_diagnostics(db, &mut diagnostics); + tracing::debug!( + target: "compiler::pipeline", + diagnostics = diagnostics.len(), + "backend pipeline completed" + ); + + Ok(CheckedHull { + program: emitted.program, + diagnostics, + }) +} + +fn normalize_backend_diagnostics(db: &dyn hir_ty::Db, diagnostics: &mut Vec) { + sort_dedup_rendered_diagnostics(db, diagnostics); +} + +fn append_stage_diagnostics( + accumulated: &mut Vec, + stage: impl IntoIterator, +) -> bool { + let start = accumulated.len(); + accumulated.extend(stage); + accumulated[start..] + .iter() + .any(|diagnostic| diagnostic.level == DiagnosticLevel::Error) +} + +/// Selects which reachable libraries contribute contract ABI documents. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AbiLibraryScope { + /// Include only contracts in the main source library. + Main, + /// Include main and external libraries, but exclude the standard library. + NonStd, +} + +impl AbiLibraryScope { + fn includes(self, library: &LibraryId) -> bool { + match self { + Self::Main => matches!(library, LibraryId::Main), + Self::NonStd => !matches!(library, LibraryId::Std), + } + } +} + +/// A structured failure encountered while collecting contract ABIs. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AbiCollectionError { + /// A reachable module was not loaded into the compiler database. + MissingModuleSource { + /// Logical module whose source is unavailable. + module: ModuleKey, + }, + /// Rendering the ABI for one contract failed. + Render { + /// Logical module containing the contract. + module: ModuleKey, + /// Source-level contract name. + contract: String, + /// ABI renderer error. + message: String, + }, + /// Two included modules contain contracts with the same output name. + NameCollision { + /// Colliding contract/output name. + name: String, + /// Module in which the name was first encountered. + first_module: ModuleKey, + /// Later module containing the same name. + second_module: ModuleKey, + }, +} + +impl fmt::Display for AbiCollectionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingModuleSource { module } => write!( + f, + "source for reachable module `{}` is unavailable while collecting contract ABIs", + display_module_key(module) + ), + Self::Render { + module, + contract, + message, + } => write!( + f, + "failed to render ABI for contract `{contract}` in `{}`: {message}", + display_module_key(module) + ), + Self::NameCollision { + name, + first_module, + second_module, + } => write!( + f, + "contract ABI name `{name}` is defined in both `{}` and `{}`", + display_module_key(first_module), + display_module_key(second_module) + ), + } + } +} + +impl Error for AbiCollectionError {} + +/// Collects ABI JSON by contract name for the requested reachable libraries. +/// +/// Name collisions are always reported and never resolved by overwriting an +/// earlier entry. Collection continues after errors so callers receive every +/// missing source, render failure, and collision in one result. Callers are +/// responsible for loading all reachable source modules before collection. +#[tracing::instrument( + target = "compiler::abi", + level = "debug", + skip_all, + fields(entry = %entry.display(db), scope = ?scope) +)] +pub fn collect_contract_abis<'db>( + db: &'db dyn hir_ty::Db, + entry: ModuleId<'db>, + scope: AbiLibraryScope, +) -> Result, Vec> { + let _ = nameres::resolve_reachable_full(db, entry); + let mut abis = BTreeMap::new(); + let mut owners = BTreeMap::::new(); + let mut errors = Vec::new(); + let mut modules_scanned = 0usize; + let mut contracts = 0usize; + let mut missing_sources = 0usize; + let mut render_failures = 0usize; + let mut name_collisions = 0usize; + + for module_id in nameres::reachable_modules(db, entry) { + if !scope.includes(module_id.library(db)) { + continue; + } + modules_scanned += 1; + let module_key = module_id.key(db); + tracing::trace!( + target: "compiler::abi", + module = %module_id.display(db), + "scanning module for contract ABIs" + ); + let Some(file) = db.module_file(module_id) else { + missing_sources += 1; + errors.push(AbiCollectionError::MissingModuleSource { module: module_key }); + continue; + }; + let module = parser::parse_file_to_hir(db, file).module(db); + for item in module.items(db) { + let Item::ContractDef(contract) = *item else { + continue; + }; + contracts += 1; + let name = contract + .def_id_value(db) + .name(db) + .unwrap_or_else(|| "Contract".to_owned()); + + let collided = match owners.entry(name.clone()) { + std::collections::btree_map::Entry::Occupied(owner) => { + name_collisions += 1; + errors.push(AbiCollectionError::NameCollision { + name: name.clone(), + first_module: owner.get().clone(), + second_module: module_key.clone(), + }); + true + } + std::collections::btree_map::Entry::Vacant(owner) => { + owner.insert(module_key.clone()); + false + } + }; + + match hir_ty::contract_abi_json(db, module, contract) { + Ok(json) if !collided => { + abis.insert(name, json); + } + Ok(_) => {} + Err(message) => { + render_failures += 1; + errors.push(AbiCollectionError::Render { + module: module_key.clone(), + contract: name, + message, + }); + } + } + } + } + + tracing::debug!( + target: "compiler::abi", + modules_scanned, + contracts, + outputs = abis.len(), + errors = errors.len(), + missing_sources, + render_failures, + name_collisions, + "contract ABI collection completed" + ); + if errors.is_empty() { + Ok(abis) + } else { + Err(errors) + } +} + +fn display_module_key(key: &ModuleKey) -> String { + let path = key.logical_path.join("."); + match &key.library { + LibraryId::Main => path, + LibraryId::Std if key.logical_path.as_slice() == ["std"] => "std".to_owned(), + LibraryId::Std => format!("std.{path}"), + LibraryId::External(name) => format!("@{name}.{path}"), + } +} + +fn source_file_name(db: &dyn hir::Db, file: SourceFile) -> String { + let url = file.url(db); + if let Some(mut segments) = url.path_segments() + && let Some(last) = segments.next_back() + && !last.is_empty() + { + return last.to_owned(); + } + url.as_str() + .rsplit('/') + .next() + .filter(|tail| !tail.is_empty()) + .unwrap_or(url.as_str()) + .to_owned() +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + }; + + use hir::{diag::DiagnosticLevel, input::SourceFile}; + use nameres::{Db as _, LibraryId, ModuleFsSnapshot, ModuleKey, module_id_from_key}; + use solcore_test_utils::{FrontendTestDb as _, define_frontend_test_db, load_main_source}; + use url::Url; + + use super::*; + + define_frontend_test_db!(TestDb, hir_ty); + + #[test] + fn stage_warnings_continue_but_errors_stop() { + let mut diagnostics = Vec::new(); + assert!(!append_stage_diagnostics( + &mut diagnostics, + [ + Diagnostic::warning("warning"), + Diagnostic::note("note"), + Diagnostic::help("help"), + ], + )); + assert_eq!(diagnostics.len(), 3); + + assert!(append_stage_diagnostics( + &mut diagnostics, + [Diagnostic::error("error")], + )); + assert_eq!(diagnostics.len(), 4); + } + + #[test] + fn backend_diagnostics_are_sorted_and_deduplicated() { + let db = TestDb::default(); + let mut diagnostics = vec![ + Diagnostic::warning("z warning"), + Diagnostic::warning("a warning"), + Diagnostic::warning("a warning"), + ]; + + normalize_backend_diagnostics(&db, &mut diagnostics); + + assert_eq!(diagnostics.len(), 2); + assert_eq!(diagnostics[0].message, "a warning"); + assert_eq!(diagnostics[1].message, "z warning"); + } + + #[test] + fn frontend_diagnostics_are_lowered() { + let mut db = TestDb::default(); + let key = load_main_source(&mut db, "function main() returns (word) { return true; }\n"); + let entry = module_id_from_key(&db, &key); + + let diagnostics = collect_frontend_diagnostics(&db, entry); + + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.level == DiagnosticLevel::Error) + ); + } + + #[test] + fn abi_collection_returns_main_contract() { + let mut db = TestDb::default(); + let key = load_main_source( + &mut db, + "contract Main { function answer() public returns (word) { return 42; } }\n", + ); + let entry = module_id_from_key(&db, &key); + + let abis = + collect_contract_abis(&db, entry, AbiLibraryScope::Main).expect("main contract ABI"); + + assert_eq!( + abis.keys().map(String::as_str).collect::>(), + ["Main"] + ); + assert!(abis["Main"].contains("\"name\": \"answer\"")); + } + + #[test] + fn clean_source_builds_checked_hull() { + let mut db = TestDb::default(); + let key = load_main_source(&mut db, "function main() returns (word) { return 42; }\n"); + let entry = module_id_from_key(&db, &key); + let file = db.module_file(entry).expect("entry source"); + + let checked = build_checked_hull(&db, file, specialize::SpecializeOptions::default()) + .expect("checked Hull"); + + assert!(checked.diagnostics.is_empty()); + assert!(!checked.program.functions.is_empty()); + } + + #[test] + fn abi_collection_reports_name_collisions_without_overwriting() { + let mut db = TestDb::default(); + let entry_key = load_main_source( + &mut db, + "import * as a from a; import * as b from b;\nfunction main() returns (word) { return 0; }\n", + ); + insert_main_module( + &mut db, + "a", + "contract Token { function main() public returns (word) { return 1; } }\n", + ); + insert_main_module( + &mut db, + "b", + "contract Token { function main() public returns (word) { return 2; } }\n", + ); + set_main_module_paths(&mut db, &["main", "a", "b"]); + let entry = module_id_from_key(&db, &entry_key); + + let errors = collect_contract_abis(&db, entry, AbiLibraryScope::Main) + .expect_err("duplicate contract ABI name"); + + assert!(matches!( + errors.as_slice(), + [AbiCollectionError::NameCollision { name, .. }] if name == "Token" + )); + } + + #[test] + fn abi_collection_reports_missing_entry_source() { + let db = TestDb::default(); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["missing".to_owned()], + }; + let entry = module_id_from_key(&db, &key); + + let errors = collect_contract_abis(&db, entry, AbiLibraryScope::Main) + .expect_err("missing module source"); + + assert_eq!( + errors, + vec![AbiCollectionError::MissingModuleSource { module: key }] + ); + } + + fn insert_main_module(db: &mut TestDb, name: &str, source: &str) { + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec![name.to_owned()], + }; + let url = Url::parse(&format!("memory:///main/{name}.solc")).expect("module URL"); + let file = SourceFile::new(db, url, Some(source.to_owned())); + db.insert_module_file(key, file); + } + + fn set_main_module_paths(db: &mut TestDb, stems: &[&str]) { + let root = PathBuf::from("/main"); + let existing_files = stems + .iter() + .map(|stem| root.join(format!("{stem}.solc"))) + .collect::>(); + let sibling_stems = + BTreeMap::from([(root, stems.iter().map(|stem| (*stem).to_owned()).collect())]); + db.set_module_fs_snapshot(ModuleFsSnapshot::new(db, existing_files, sibling_stems)); + } +} diff --git a/crates/driver/Cargo.toml b/crates/driver/Cargo.toml index 624cc8e5..268220a8 100644 --- a/crates/driver/Cargo.toml +++ b/crates/driver/Cargo.toml @@ -2,9 +2,24 @@ name = "solcore-driver" version = "0.1.0" edition.workspace = true +rust-version.workspace = true [dependencies] +annotate-snippets = { workspace = true } +compiler = { workspace = true } +libc = "0.2" salsa = { workspace = true } +rustc-hash = { workspace = true } url = { workspace = true } hir = { workspace = true } +hir-ty = { workspace = true } +hull = { path = "../hull", package = "solcore-hull" } parser = { workspace = true } +nameres = { workspace = true } +specialize = { path = "../specialize", package = "solcore-specialize" } +sonatina = { path = "../sonatina", package = "solcore-sonatina" } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +yul = { path = "../yul", package = "solcore-yul" } +vfs = { workspace = true } +serde_json = "1" diff --git a/crates/driver/src/args.rs b/crates/driver/src/args.rs new file mode 100644 index 00000000..860e1386 --- /dev/null +++ b/crates/driver/src/args.rs @@ -0,0 +1,645 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + path::PathBuf, +}; + +const DEFAULT_DIAGNOSTIC_WIDTH: usize = 100; + +pub(crate) enum ParsedArgs { + Run(Box), + StandardJson, + Help, + Version, +} + +/// Parsed command-line arguments for a compiler run. +pub(crate) struct Args { + /// Input source file. + pub(crate) input: PathBuf, + /// Optional main library root override. + pub(crate) main_root: Option, + /// Optional std library root override. + pub(crate) std_root: Option, + /// External library roots passed as `NAME=PATH`. + pub(crate) external_roots: Vec<(String, PathBuf)>, + /// Enables compact tracing output when `RUST_LOG` is not set. + pub(crate) trace: bool, + /// Diagnostic color policy. + pub(crate) color: ColorChoice, + /// Diagnostic Unicode decoration policy. + pub(crate) unicode: UnicodeChoice, + /// Diagnostic output width, if explicitly configured. + pub(crate) diagnostic_width: Option, + /// Diagnostic output format. + pub(crate) diagnostic_format: DiagnosticFormat, + /// Warning rendering/escalation policy. + pub(crate) warning_policy: WarningPolicy, + /// Optional output directory for emitted artifact files. + pub(crate) output_dir: Option, + /// Emits one ABI JSON file per reachable local contract. + pub(crate) emit_abi: bool, + /// Optional Hull output target. + pub(crate) emit_hull: Option, + /// Optional Yul output target. + pub(crate) emit_yul: Option, + /// Optional Sonatina IR output target. + pub(crate) emit_sonatina: Option, + /// Optional top-level Yul object selection for strict-assembly output. + pub(crate) emit_yul_object: Option, + /// Resource limits used by monomorphization and partial evaluation. + pub(crate) specialize_options: specialize::SpecializeOptions, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum EmitTarget { + Stdout, + File(PathBuf), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ColorChoice { + Auto, + Always, + Never, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum UnicodeChoice { + Auto, + Always, + Never, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DiagnosticFormat { + Human, + Short, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum WarningPolicy { + Default, + Always, + Never, + Deny, +} + +/// Parses command-line arguments. +/// +/// The driver accepts exactly one input file and zero or more external library +/// roots via `--external-lib NAME=PATH`, `--external-lib=NAME=PATH`, `--lib`, +/// or `--lib=`. +pub(crate) fn parse_args(args: Vec) -> Result { + if args.len() == 1 && args[0] == OsStr::new("--standard-json") { + return Ok(ParsedArgs::StandardJson); + } + if args.iter().any(|arg| arg == OsStr::new("--standard-json")) { + return Err("--standard-json must be used without other options".to_owned()); + } + + let mut input = None; + let mut main_root = None; + let mut std_root = None; + let mut external_roots = Vec::new(); + let mut trace = false; + let mut color = ColorChoice::Auto; + let mut unicode = UnicodeChoice::Auto; + let mut diagnostic_width = None; + let mut diagnostic_format = DiagnosticFormat::Human; + let mut warning_policy = WarningPolicy::Default; + let mut output_dir = None; + let mut emit_abi = false; + let mut emit_hull = None; + let mut emit_yul = None; + let mut emit_sonatina = None; + let mut emit_yul_object = None; + let mut specialize_options = specialize::SpecializeOptions::default(); + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + if arg.to_str().is_none() { + if let Some(value) = strip_os_prefix(&arg, "--file=") { + if value.as_os_str().is_empty() { + return Err("--file= requires FILE".to_owned()); + } + set_input(&mut input, PathBuf::from(value))?; + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--emit-hull=") { + if value.as_os_str().is_empty() { + return Err("--emit-hull= requires FILE".to_owned()); + } + emit_hull = Some(EmitTarget::File(PathBuf::from(value))); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--emit-yul=") { + if value.as_os_str().is_empty() { + return Err("--emit-yul= requires FILE".to_owned()); + } + emit_yul = Some(EmitTarget::File(PathBuf::from(value))); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--emit-sonatina=") { + if value.as_os_str().is_empty() { + return Err("--emit-sonatina= requires FILE".to_owned()); + } + emit_sonatina = Some(EmitTarget::File(PathBuf::from(value))); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--root=") { + if value.as_os_str().is_empty() { + return Err("--root= requires DIR".to_owned()); + } + main_root = Some(PathBuf::from(value)); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--std-root=") { + if value.as_os_str().is_empty() { + return Err("--std-root= requires DIR".to_owned()); + } + std_root = Some(PathBuf::from(value)); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--include=") { + if value.as_os_str().is_empty() { + return Err("--include= requires DIR".to_owned()); + } + std_root = Some(PathBuf::from(value)); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--output-dir=") { + if value.as_os_str().is_empty() { + return Err("--output-dir= requires DIR".to_owned()); + } + output_dir = Some(PathBuf::from(value)); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--external-lib=") { + external_roots.push(parse_external_root(value)?); + continue; + } + if let Some(value) = strip_os_prefix(&arg, "--lib=") { + external_roots.push(parse_external_root(value)?); + continue; + } + if os_arg_starts_with(&arg, "-") { + return Err(format!( + "unknown non-UTF-8 option `{}`", + arg.to_string_lossy() + )); + } + set_input(&mut input, PathBuf::from(arg))?; + continue; + } + + let arg_str = arg.to_str(); + match arg_str { + Some("-h" | "--help") => return Ok(ParsedArgs::Help), + Some("-V" | "--version") => return Ok(ParsedArgs::Version), + Some("--trace") => { + trace = true; + } + Some(option @ ("-f" | "--file")) => { + let value = next_path_option_value(&mut iter, option, "FILE")?; + set_input(&mut input, value)?; + } + Some("--root") => { + main_root = Some(next_path_option_value(&mut iter, "--root", "DIR")?); + } + Some(option @ ("--std-root" | "--include" | "-i")) => { + std_root = Some(next_path_option_value(&mut iter, option, "DIR")?); + } + Some("--color") => { + let value = next_string_option_value(&mut iter, "--color", "auto|always|never")?; + color = parse_color_choice(&value)?; + } + Some("--unicode") => { + let value = next_string_option_value(&mut iter, "--unicode", "auto|always|never")?; + unicode = parse_unicode_choice(&value)?; + } + Some("--diagnostic-width") => { + let value = next_string_option_value(&mut iter, "--diagnostic-width", "N")?; + diagnostic_width = Some(parse_diagnostic_width(&value)?); + } + Some("--diagnostic-format") => { + let value = + next_string_option_value(&mut iter, "--diagnostic-format", "human|short")?; + diagnostic_format = parse_diagnostic_format(&value)?; + } + Some("--warnings") => { + let value = + next_string_option_value(&mut iter, "--warnings", "default|always|never|deny")?; + warning_policy = parse_warning_policy(&value)?; + } + Some(option @ ("-o" | "--output-dir")) => { + output_dir = Some(next_path_option_value(&mut iter, option, "DIR")?); + } + Some("--abi") => { + emit_abi = true; + } + Some("--emit-hull") => { + emit_hull = Some(EmitTarget::Stdout); + } + Some("--emit-yul") => { + emit_yul = Some(EmitTarget::Stdout); + } + Some("--emit-sonatina") => { + emit_sonatina = Some(EmitTarget::Stdout); + } + Some("--emit-yul-object") => { + let value = next_string_option_value(&mut iter, "--emit-yul-object", "NAME")?; + emit_yul_object = Some(value); + } + Some("--pe-fuel") => { + let value = next_string_option_value(&mut iter, "--pe-fuel", "N")?; + specialize_options.eval_fuel = parse_positive_limit("--pe-fuel", &value)?; + } + Some("--pe-depth") => { + let value = next_string_option_value(&mut iter, "--pe-depth", "N")?; + specialize_options.max_depth = parse_positive_limit("--pe-depth", &value)?; + } + Some("--pe-max-instantiations") => { + let value = next_string_option_value(&mut iter, "--pe-max-instantiations", "N")?; + specialize_options.max_instantiations = + parse_positive_limit("--pe-max-instantiations", &value)?; + } + Some("--pe-max-type-nodes") => { + let value = next_string_option_value(&mut iter, "--pe-max-type-nodes", "N")?; + specialize_options.max_type_nodes = + parse_positive_limit("--pe-max-type-nodes", &value)?; + } + Some(option @ ("--external-lib" | "--lib")) => { + let value = next_os_option_value(&mut iter, option, "NAME=PATH")?; + external_roots.push(parse_external_root(value)?); + } + Some(arg) if arg.starts_with("--emit-yul-object=") => { + let value = &arg["--emit-yul-object=".len()..]; + if value.is_empty() { + return Err("--emit-yul-object= requires NAME".to_owned()); + } + emit_yul_object = Some(value.to_owned()); + } + Some(arg) if arg.starts_with("--pe-fuel=") => { + specialize_options.eval_fuel = + parse_positive_limit("--pe-fuel", &arg["--pe-fuel=".len()..])?; + } + Some(arg) if arg.starts_with("--pe-depth=") => { + specialize_options.max_depth = + parse_positive_limit("--pe-depth", &arg["--pe-depth=".len()..])?; + } + Some(arg) if arg.starts_with("--pe-max-instantiations=") => { + specialize_options.max_instantiations = parse_positive_limit( + "--pe-max-instantiations", + &arg["--pe-max-instantiations=".len()..], + )?; + } + Some(arg) if arg.starts_with("--pe-max-type-nodes=") => { + specialize_options.max_type_nodes = parse_positive_limit( + "--pe-max-type-nodes", + &arg["--pe-max-type-nodes=".len()..], + )?; + } + Some(arg) if arg.starts_with("--color=") => { + color = parse_color_choice(&arg["--color=".len()..])?; + } + Some(arg) if arg.starts_with("--unicode=") => { + unicode = parse_unicode_choice(&arg["--unicode=".len()..])?; + } + Some(arg) if arg.starts_with("--diagnostic-width=") => { + diagnostic_width = + Some(parse_diagnostic_width(&arg["--diagnostic-width=".len()..])?); + } + Some(arg) if arg.starts_with("--diagnostic-format=") => { + diagnostic_format = parse_diagnostic_format(&arg["--diagnostic-format=".len()..])?; + } + Some(arg) if arg.starts_with("--warnings=") => { + warning_policy = parse_warning_policy(&arg["--warnings=".len()..])?; + } + Some(arg) if arg.starts_with("--file=") => { + let value = &arg["--file=".len()..]; + if value.is_empty() { + return Err("--file= requires FILE".to_owned()); + } + set_input(&mut input, PathBuf::from(value))?; + } + Some(arg) if arg.starts_with("--emit-hull=") => { + let value = &arg["--emit-hull=".len()..]; + if value.is_empty() { + return Err("--emit-hull= requires FILE".to_owned()); + } + emit_hull = Some(EmitTarget::File(PathBuf::from(value))); + } + Some(arg) if arg.starts_with("--emit-yul=") => { + let value = &arg["--emit-yul=".len()..]; + if value.is_empty() { + return Err("--emit-yul= requires FILE".to_owned()); + } + emit_yul = Some(EmitTarget::File(PathBuf::from(value))); + } + Some(arg) if arg.starts_with("--emit-sonatina=") => { + let value = &arg["--emit-sonatina=".len()..]; + if value.is_empty() { + return Err("--emit-sonatina= requires FILE".to_owned()); + } + emit_sonatina = Some(EmitTarget::File(PathBuf::from(value))); + } + Some(arg) if arg.starts_with("--root=") => { + let value = &arg["--root=".len()..]; + if value.is_empty() { + return Err("--root= requires DIR".to_owned()); + } + main_root = Some(PathBuf::from(value)); + } + Some(arg) if arg.starts_with("--std-root=") => { + let value = &arg["--std-root=".len()..]; + if value.is_empty() { + return Err("--std-root= requires DIR".to_owned()); + } + std_root = Some(PathBuf::from(value)); + } + Some(arg) if arg.starts_with("--include=") => { + let value = &arg["--include=".len()..]; + if value.is_empty() { + return Err("--include= requires DIR".to_owned()); + } + std_root = Some(PathBuf::from(value)); + } + Some(arg) if arg.starts_with("--output-dir=") => { + let value = &arg["--output-dir=".len()..]; + if value.is_empty() { + return Err("--output-dir= requires DIR".to_owned()); + } + output_dir = Some(PathBuf::from(value)); + } + Some(arg) if arg.starts_with("--external-lib=") => { + external_roots.push(parse_external_root(OsString::from( + &arg["--external-lib=".len()..], + ))?); + } + Some(arg) if arg.starts_with("--lib=") => { + external_roots.push(parse_external_root(OsString::from(&arg["--lib=".len()..]))?); + } + Some(arg) if arg.starts_with('-') => { + return Err(format!("unknown option `{arg}`")); + } + _ => { + set_input(&mut input, PathBuf::from(arg))?; + } + } + } + + let Some(input) = input else { + return Err("missing input file".to_owned()); + }; + if emit_yul_object.is_some() && emit_yul.is_none() { + return Err("--emit-yul-object requires --emit-yul".to_owned()); + } + Ok(ParsedArgs::Run(Box::new(Args { + input, + main_root, + std_root, + external_roots, + trace, + color, + unicode, + diagnostic_width, + diagnostic_format, + warning_policy, + output_dir, + emit_abi, + emit_hull, + emit_yul, + emit_sonatina, + emit_yul_object, + specialize_options, + }))) +} + +fn next_os_option_value( + iter: &mut impl Iterator, + option: &str, + value_name: &str, +) -> Result { + let Some(value) = iter.next() else { + return Err(format!("{option} requires {value_name}")); + }; + if value.as_os_str().is_empty() { + return Err(format!("{option} requires {value_name}")); + } + Ok(value) +} + +fn set_input(input: &mut Option, value: PathBuf) -> Result<(), String> { + if input.replace(value).is_some() { + return Err("expected exactly one input file".to_owned()); + } + Ok(()) +} + +fn next_path_option_value( + iter: &mut impl Iterator, + option: &str, + value_name: &str, +) -> Result { + next_os_option_value(iter, option, value_name).map(PathBuf::from) +} + +fn next_string_option_value( + iter: &mut impl Iterator, + option: &str, + value_name: &str, +) -> Result { + let value = next_os_option_value(iter, option, value_name)?; + os_value_to_string(&value, option) +} + +fn os_value_to_string(value: &OsStr, option: &str) -> Result { + value + .to_str() + .map(ToOwned::to_owned) + .ok_or_else(|| format!("{option} requires a UTF-8 value")) +} + +fn strip_os_prefix(arg: &OsStr, prefix: &str) -> Option { + #[cfg(unix)] + { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + arg.as_bytes() + .strip_prefix(prefix.as_bytes()) + .map(|value| OsString::from_vec(value.to_vec())) + } + #[cfg(not(unix))] + { + arg.to_str() + .and_then(|value| value.strip_prefix(prefix)) + .map(OsString::from) + } +} + +fn os_arg_starts_with(arg: &OsStr, prefix: &str) -> bool { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + arg.as_bytes().starts_with(prefix.as_bytes()) + } + #[cfg(not(unix))] + { + arg.to_str().is_some_and(|value| value.starts_with(prefix)) + } +} + +fn parse_external_root(value: OsString) -> Result<(String, PathBuf), String> { + #[cfg(unix)] + { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + let raw = value.as_os_str().as_bytes(); + let Some(eq) = raw.iter().position(|byte| *byte == b'=') else { + return Err(format!( + "external library must be NAME=PATH, got `{}`", + value.to_string_lossy() + )); + }; + let (name, path) = raw.split_at(eq); + let path = &path[1..]; + if name.is_empty() || path.is_empty() { + return Err(format!( + "external library must be NAME=PATH, got `{}`", + value.to_string_lossy() + )); + } + let name = std::str::from_utf8(name) + .map_err(|_| "external library name must be UTF-8".to_owned())?; + Ok(( + name.to_owned(), + PathBuf::from(OsString::from_vec(path.to_vec())), + )) + } + #[cfg(not(unix))] + { + let value = os_value_to_string(&value, "--external-lib")?; + let Some((name, path)) = value.split_once('=') else { + return Err(format!("external library must be NAME=PATH, got `{value}`")); + }; + if name.is_empty() || path.is_empty() { + return Err(format!("external library must be NAME=PATH, got `{value}`")); + } + Ok((name.to_owned(), PathBuf::from(path))) + } +} + +fn parse_color_choice(value: &str) -> Result { + match value { + "auto" => Ok(ColorChoice::Auto), + "always" => Ok(ColorChoice::Always), + "never" => Ok(ColorChoice::Never), + _ => Err(format!( + "--color must be one of auto, always, or never, got `{value}`" + )), + } +} + +fn parse_unicode_choice(value: &str) -> Result { + match value { + "auto" => Ok(UnicodeChoice::Auto), + "always" => Ok(UnicodeChoice::Always), + "never" => Ok(UnicodeChoice::Never), + _ => Err(format!( + "--unicode must be one of auto, always, or never, got `{value}`" + )), + } +} + +fn parse_diagnostic_width(value: &str) -> Result { + let width = value + .parse::() + .map_err(|_| format!("--diagnostic-width requires a positive integer, got `{value}`"))?; + if width == 0 { + return Err("--diagnostic-width requires a positive integer, got `0`".to_owned()); + } + Ok(width) +} + +fn parse_positive_limit(option: &str, value: &str) -> Result { + let limit = value + .parse::() + .map_err(|_| format!("{option} requires a positive integer, got `{value}`"))?; + if limit == 0 { + return Err(format!("{option} requires a positive integer, got `0`")); + } + Ok(limit) +} + +fn parse_diagnostic_format(value: &str) -> Result { + match value { + "human" => Ok(DiagnosticFormat::Human), + "short" => Ok(DiagnosticFormat::Short), + _ => Err(format!( + "--diagnostic-format must be one of human or short, got `{value}`" + )), + } +} + +fn parse_warning_policy(value: &str) -> Result { + match value { + "default" => Ok(WarningPolicy::Default), + "always" => Ok(WarningPolicy::Always), + "never" => Ok(WarningPolicy::Never), + "deny" => Ok(WarningPolicy::Deny), + _ => Err(format!( + "--warnings must be one of default, always, never, or deny, got `{value}`" + )), + } +} + +pub(crate) fn default_diagnostic_width() -> usize { + env::var("COLUMNS") + .ok() + .and_then(|value| value.parse::().ok()) + .map(|width| width.max(20)) + .unwrap_or(DEFAULT_DIAGNOSTIC_WIDTH) +} + +pub(crate) fn usage_text(program: &str) -> String { + format!("usage: {program} [OPTIONS] \ntry `{program} --help` for more information") +} + +pub(crate) fn help_text(program: &str) -> String { + format!( + "\ +Solcore Rust driver + +Usage: {program} [OPTIONS] [] + +Options: + -f, --file FILE Input source file (alternative to positional input) + --root DIR Set the main library root (default: input file directory) + --std-root DIR Set the std library root + -i, --include DIR Alias for --std-root + --external-lib NAME=PATH Register an external library root for @NAME imports + --lib NAME=PATH Alias for --external-lib + -o, --output-dir DIR Directory for emitted artifact and ABI files + --abi Emit a JSON ABI file for each contract + --emit-hull[=FILE] Emit Hull to stdout or FILE + --emit-yul[=FILE] Emit Yul strict assembly to stdout or FILE + --emit-sonatina[=FILE] Emit Sonatina IR to stdout or FILE + --emit-yul-object NAME Select one top-level Yul object for --emit-yul + --pe-fuel N Set partial-evaluation total work fuel (default: 4096) + --pe-depth N Set specialization/evaluator depth (default: 128) + --pe-max-instantiations N Set specialization instance limit (default: 2048) + --pe-max-type-nodes N Set specialized type-size limit (default: 4096) + --color auto|always|never Configure diagnostic colors (default: auto) + --unicode auto|always|never Configure diagnostic Unicode output (default: auto) + --diagnostic-width N Set diagnostic output width (default: 100) + --diagnostic-format human|short Configure diagnostic output format (default: human) + --warnings default|always|never|deny + Configure compiler warning diagnostics (default: default) + --trace Enable compact compiler tracing + --standard-json Read Solcore Standard JSON from stdin + -h, --help Show this help text + -V, --version Show version information + +Std root resolution order: + --std-root, SOLCORE_STD, /std, dev checkout std +" + ) +} diff --git a/crates/driver/src/db.rs b/crates/driver/src/db.rs new file mode 100644 index 00000000..342bfc23 --- /dev/null +++ b/crates/driver/src/db.rs @@ -0,0 +1,112 @@ +use hir::input::SourceFile; +use nameres::{ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree}; +use parser::parse_file_to_hir; +use rustc_hash::FxHashMap; +use salsa::Setter; +use tracing::Level; + +use crate::trace::emit_salsa_event; + +/// Concrete Salsa database used by the command-line driver. +/// +/// The database wires HIR, parser, and inter-module name-resolution traits +/// together and stores the loaded module files discovered from imports. +#[salsa::db] +#[derive(Clone)] +pub(crate) struct DriverDb { + /// Salsa storage. + storage: salsa::Storage, + /// Module roots for the current run. + pub(crate) module_tree: Option, + /// Filesystem facts used by module path resolution. + pub(crate) module_fs_snapshot: Option, + /// Tracked snapshot of `module_files` consumed by name resolution. + pub(crate) module_file_snapshot: Option, + /// Loaded source file for each logical module key. + pub(crate) module_files: FxHashMap, +} + +impl DriverDb { + pub(crate) fn new() -> Self { + Self { + storage: salsa::Storage::new( + if tracing::enabled!(target: "driver::salsa", Level::DEBUG) { + Some(Box::new(emit_salsa_event)) + } else { + None + }, + ), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: FxHashMap::default(), + } + } +} + +impl Default for DriverDb { + fn default() -> Self { + Self::new() + } +} + +impl DriverDb { + pub(crate) fn sync_module_file_snapshot(&mut self) { + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + if snapshot.files(self) != &files { + snapshot.set_files(self).to(files); + } + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for DriverDb {} + +#[salsa::db] +impl hir::Db for DriverDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for DriverDb {} + +#[salsa::db] +impl nameres::Db for DriverDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree + .expect("DriverDb module tree is initialized before use") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("DriverDb module filesystem snapshot is initialized before use") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("DriverDb module file snapshot is initialized before use") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl hir_ty::Db for DriverDb {} diff --git a/crates/driver/src/diagnostics.rs b/crates/driver/src/diagnostics.rs new file mode 100644 index 00000000..9dd9183c --- /dev/null +++ b/crates/driver/src/diagnostics.rs @@ -0,0 +1,112 @@ +use std::{env, io::IsTerminal}; + +use annotate_snippets::{Renderer, renderer::DecorStyle}; +use hir::diag::{Diagnostic, DiagnosticLevel}; + +use crate::args::{ + Args, ColorChoice, DiagnosticFormat, UnicodeChoice, WarningPolicy, default_diagnostic_width, +}; + +fn diagnostic_renderer(args: &Args) -> Renderer { + let renderer = match args.color { + ColorChoice::Always => Renderer::styled(), + ColorChoice::Never => Renderer::plain(), + ColorChoice::Auto => { + let no_color = env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()); + if !no_color && std::io::stderr().is_terminal() { + Renderer::styled() + } else { + Renderer::plain() + } + } + }; + renderer + .term_width( + args.diagnostic_width + .unwrap_or_else(default_diagnostic_width), + ) + .decor_style(match args.unicode { + UnicodeChoice::Always => DecorStyle::Unicode, + UnicodeChoice::Never => DecorStyle::Ascii, + UnicodeChoice::Auto if std::io::stderr().is_terminal() => DecorStyle::Unicode, + UnicodeChoice::Auto => DecorStyle::Ascii, + }) +} + +pub(crate) fn render_diagnostics( + db: &dyn hir::Db, + diagnostics: &[Diagnostic], + args: &Args, +) -> String { + match args.diagnostic_format { + DiagnosticFormat::Human => { + let renderer = diagnostic_renderer(args); + render_diagnostic_blocks( + diagnostics + .iter() + .map(|diagnostic| diagnostic.render_with(db, &renderer)), + ) + } + DiagnosticFormat::Short => diagnostics + .iter() + .map(|diagnostic| diagnostic.render_short(db)) + .collect(), + } +} + +fn render_diagnostic_blocks(rendered_blocks: impl IntoIterator) -> String { + let mut output = String::new(); + for rendered in rendered_blocks { + if !output.is_empty() { + output.push('\n'); + } + output.push_str(&normalize_rendered_diagnostic(rendered)); + } + output +} + +fn normalize_rendered_diagnostic(mut rendered: String) -> String { + while rendered.ends_with('\n') { + rendered.pop(); + } + rendered.push('\n'); + rendered +} + +pub(crate) fn apply_warning_policy(diagnostics: &mut Vec, policy: WarningPolicy) { + match policy { + WarningPolicy::Default | WarningPolicy::Always => {} + WarningPolicy::Never => { + diagnostics.retain(|diagnostic| diagnostic.level != DiagnosticLevel::Warning); + } + WarningPolicy::Deny => { + for diagnostic in diagnostics + .iter_mut() + .filter(|diagnostic| diagnostic.level == DiagnosticLevel::Warning) + { + diagnostic.level = DiagnosticLevel::Error; + diagnostic.notes.push( + "pass --warnings=default, --warnings=always, or --warnings=never to allow this warning" + .to_owned(), + ); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rendered_diagnostic_blocks_have_rustc_style_spacing() { + assert_eq!( + render_diagnostic_blocks(["error: one".to_owned()]), + "error: one\n" + ); + assert_eq!( + render_diagnostic_blocks(["error: one\n\n".to_owned(), "error: two".to_owned()]), + "error: one\n\nerror: two\n" + ); + } +} diff --git a/crates/driver/src/emit.rs b/crates/driver/src/emit.rs new file mode 100644 index 00000000..1472a1a9 --- /dev/null +++ b/crates/driver/src/emit.rs @@ -0,0 +1,156 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use hir::{diag::Diagnostic, input::SourceFile}; +use nameres::ModuleId; + +use crate::{ + args::{Args, EmitTarget}, + db::DriverDb, +}; + +#[derive(Debug)] +pub(crate) enum BackendFailure { + Diagnostics(Vec), + Message(String), +} + +pub(crate) fn maybe_emit_abi_outputs( + db: &DriverDb, + entry: ModuleId<'_>, + args: &Args, +) -> Result<(), String> { + if !args.emit_abi { + return Ok(()); + } + + let outputs = compiler::collect_contract_abis(db, entry, compiler::AbiLibraryScope::Main) + .map_err(|errors| { + errors + .into_iter() + .map(format_abi_collection_error) + .collect::>() + .join("\n") + })?; + for (name, abi) in outputs { + let filename = format!("{name}.abi"); + write_output_file(&PathBuf::from(filename), args.output_dir.as_deref(), &abi)?; + } + Ok(()) +} + +fn format_abi_collection_error(error: compiler::AbiCollectionError) -> String { + match error { + compiler::AbiCollectionError::MissingModuleSource { module } => format!( + "source for reachable module `{}` is unavailable while collecting contract ABIs", + module.logical_path.join(".") + ), + compiler::AbiCollectionError::Render { + contract, message, .. + } => format!("failed to render ABI for contract `{contract}`: {message}"), + compiler::AbiCollectionError::NameCollision { + name, + first_module, + second_module, + } => { + let filename = format!("{name}.abi"); + let first_module = first_module.logical_path.join("."); + let second_module = second_module.logical_path.join("."); + format!( + "cannot emit `{filename}` for contracts named `{name}` in both `{first_module}` and `{second_module}`; rename one contract to give each ABI a unique output filename" + ) + } + } +} + +pub(crate) fn maybe_emit_backend_outputs( + db: &DriverDb, + entry_file: SourceFile, + args: &Args, +) -> Result, BackendFailure> { + if args.emit_hull.is_none() && args.emit_yul.is_none() && args.emit_sonatina.is_none() { + return Ok(Vec::new()); + } + + let stdout_backends = [ + ("--emit-hull", &args.emit_hull), + ("--emit-yul", &args.emit_yul), + ("--emit-sonatina", &args.emit_sonatina), + ] + .into_iter() + .filter(|(_, target)| matches!(target.as_ref(), Some(EmitTarget::Stdout))) + .map(|(option, _)| option) + .collect::>(); + if stdout_backends.len() > 1 { + return Err(BackendFailure::Message(format!( + "cannot write multiple backend outputs to stdout: {}", + stdout_backends.join(", ") + ))); + } + + let compiler::CheckedHull { + program, + diagnostics, + } = compiler::build_checked_hull(db, entry_file, args.specialize_options) + .map_err(BackendFailure::Diagnostics)?; + + if let Some(target) = &args.emit_hull { + write_emit_output( + target, + args.output_dir.as_deref(), + &hull::pretty_program(db, &program), + )?; + } + if let Some(target) = &args.emit_yul { + let yul = yul::render_hull_program_object(db, &program, args.emit_yul_object.as_deref()) + .map_err(|err| BackendFailure::Message(format!("Yul translation failed:\n {err}")))?; + write_emit_output(target, args.output_dir.as_deref(), &yul)?; + } + if let Some(target) = &args.emit_sonatina { + let sonatina = sonatina::render_hull_program(db, &program).map_err(|err| { + BackendFailure::Message(format!("Sonatina translation failed:\n {err}")) + })?; + write_emit_output(target, args.output_dir.as_deref(), &sonatina)?; + } + Ok(diagnostics) +} + +fn write_emit_output( + target: &EmitTarget, + output_dir: Option<&Path>, + content: &str, +) -> Result<(), BackendFailure> { + match target { + EmitTarget::Stdout => { + print!("{content}"); + Ok(()) + } + EmitTarget::File(path) => { + write_output_file(path, output_dir, content).map_err(BackendFailure::Message) + } + } +} + +fn write_output_file(path: &Path, output_dir: Option<&Path>, content: &str) -> Result<(), String> { + let path = emit_file_path(path, output_dir); + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + fs::create_dir_all(parent) + .map_err(|err| format!("failed to create `{}`: {err}", parent.display()))?; + } + fs::write(&path, content).map_err(|err| format!("failed to write `{}`: {err}", path.display())) +} + +fn emit_file_path(path: &Path, output_dir: Option<&Path>) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else if let Some(output_dir) = output_dir { + output_dir.join(path) + } else { + path.to_path_buf() + } +} diff --git a/crates/driver/src/main.rs b/crates/driver/src/main.rs index 677a2ddd..276d113b 100644 --- a/crates/driver/src/main.rs +++ b/crates/driver/src/main.rs @@ -1,75 +1,50 @@ -use std::{env, fs, path::Path}; - -use hir::{diag::Diagnostic, input::SourceFile}; -use parser::parse_file_to_hir; -use url::Url; - -#[salsa::db] -#[derive(Default, Clone)] -struct DriverDb { - storage: salsa::Storage, -} - -#[salsa::db] -impl salsa::Database for DriverDb {} - -#[salsa::db] -impl hir::Db for DriverDb {} - -#[salsa::db] -impl parser::Db for DriverDb {} - +//! Command-line driver for parsing and resolving Solcore modules. +//! +//! The driver owns filesystem concerns: argument parsing, root selection, +//! loading reachable modules into the Salsa database, and rendering pull-style +//! diagnostics. Compiler crates stay pure and receive source files through +//! database inputs. + +mod args; +mod db; +mod diagnostics; +mod emit; +mod modules; +mod paths; +mod pipeline; +mod standard_json; +mod trace; + +use std::{process, thread}; + +/// Stack size for the compilation thread. Recursive-descent parsing, HIR +/// lowering, and type folding recurse with input nesting depth; the default +/// main-thread stack overflows on deeply nested (but well-formed) programs. +const COMPILER_STACK_SIZE: usize = 256 * 1024 * 1024; + +/// Entry point for the CLI driver. +/// +/// Restores default SIGPIPE handling so piping output into e.g. `head` ends +/// the process instead of panicking, then runs the compiler on a thread with +/// a large stack. fn main() { - let mut args = env::args(); - let program = args.next().unwrap_or_else(|| "solcore-driver".to_owned()); - let Some(path_arg) = args.next() else { - eprintln!("usage: {program} "); - std::process::exit(2); - }; - if args.next().is_some() { - eprintln!("usage: {program} "); - std::process::exit(2); + #[cfg(unix)] + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); } - - let path = Path::new(&path_arg); - let canonical_path = match path.canonicalize() { - Ok(path) => path, - Err(err) => { - eprintln!("failed to resolve `{}`: {err}", path.display()); - std::process::exit(1); - } - }; - - let source = match fs::read_to_string(&canonical_path) { - Ok(source) => source, + let compiler = match thread::Builder::new() + .name("solcore-compiler".to_owned()) + .stack_size(COMPILER_STACK_SIZE) + .spawn(pipeline::run_compiler) + { + Ok(compiler) => compiler, Err(err) => { - eprintln!("failed to read `{}`: {err}", canonical_path.display()); - std::process::exit(1); - } - }; - - let url = match Url::from_file_path(&canonical_path) { - Ok(url) => url, - Err(()) => { - eprintln!( - "failed to convert `{}` into file URL", - canonical_path.display() - ); - std::process::exit(1); + eprintln!("failed to spawn compiler thread: {err}"); + process::exit(1); } }; - - let db = DriverDb::default(); - let file = SourceFile::new(&db, url, Some(source)); - let _ = parse_file_to_hir(&db, file).module(&db); - - let diagnostics = parse_file_to_hir::accumulated::(&db, file); - if diagnostics.is_empty() { - return; - } - - for diag in diagnostics { - eprint!("{}", diag.render(&db)); + let result = compiler.join(); + if let Err(payload) = result { + std::panic::resume_unwind(payload); } - std::process::exit(1); } diff --git a/crates/driver/src/modules.rs b/crates/driver/src/modules.rs new file mode 100644 index 00000000..6bd41d7e --- /dev/null +++ b/crates/driver/src/modules.rs @@ -0,0 +1,143 @@ +use std::{collections::VecDeque, fs}; + +use nameres::{LibraryId, ModuleKey, module_id_from_key, resolve_module_path_candidate}; +use rustc_hash::FxHashSet; + +use crate::{db::DriverDb, paths::source_file_for_path}; + +#[tracing::instrument( + target = "driver::modules", + level = "debug", + skip_all, + fields(entry = %module_key_display(&entry)) +)] +pub(crate) fn load_reachable_modules(db: &mut DriverDb, entry: ModuleKey) -> Result<(), String> { + let mut queue = VecDeque::from([entry]); + let mut visited = FxHashSet::default(); + let mut loaded = 0usize; + + while let Some(key) = queue.pop_front() { + if !visited.insert(key.clone()) { + continue; + } + tracing::trace!( + target: "driver::modules", + module = %module_key_display(&key), + "visiting reachable module" + ); + let Some(file) = db.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map( + |path| match resolve_module_path_candidate(&*db, module, &path) { + Ok(resolved) => { + tracing::trace!( + target: "driver::modules", + module = %module.display(&*db), + path = %nameres::module_path_display(&*db, &path), + target = %resolved.module.display(&*db), + file = %resolved.file_path.display(), + "discovered module reference" + ); + Some((resolved.module.key(&*db), resolved.file_path)) + } + Err(_) => { + tracing::trace!( + target: "driver::modules", + module = %module.display(&*db), + path = %nameres::module_path_display(&*db, &path), + "ignored unresolved module reference" + ); + None + } + }, + ) + .collect::>() + }; + for (target_key, file_path) in targets { + if !db.module_files.contains_key(&target_key) { + validate_external_root_dir(db, &target_key)?; + match fs::read_to_string(&file_path) { + Ok(source) => match source_file_for_path(db, &file_path, source) { + Ok(file) => { + tracing::debug!( + target: "driver::modules", + module = %module_key_display(&target_key), + file = %file_path.display(), + "loaded module source" + ); + db.module_files.insert(target_key.clone(), file); + loaded += 1; + } + Err(message) => { + return Err(format!( + "failed to load module `{}` from `{}`: {message}", + module_key_display(&target_key), + file_path.display() + )); + } + }, + Err(err) => { + return Err(format!( + "failed to read module `{}` from `{}`: {err}", + module_key_display(&target_key), + file_path.display() + )); + } + } + } + if db.module_files.contains_key(&target_key) { + queue.push_back(target_key); + } + } + } + db.sync_module_file_snapshot(); + tracing::debug!( + target: "driver::modules", + visited = visited.len(), + loaded, + available = db.module_files.len(), + "reachable module loading completed" + ); + Ok(()) +} + +fn validate_external_root_dir(db: &DriverDb, target_key: &ModuleKey) -> Result<(), String> { + let LibraryId::External(name) = &target_key.library else { + return Ok(()); + }; + let tree = db + .module_tree + .expect("DriverDb module tree is initialized before use"); + let Some(root) = tree.external_roots(db).get(name) else { + return Ok(()); + }; + if root.is_dir() { + return Ok(()); + } + let problem = if root.exists() { + "is not a directory" + } else { + "does not exist" + }; + Err(format!( + "external library `@{name}` root directory {problem}: `{}`\nnote: pass --external-lib {name}=PATH with an existing directory", + root.display() + )) +} + +fn module_key_display(key: &ModuleKey) -> String { + let path = key.logical_path.join("."); + match &key.library { + LibraryId::Main => path, + LibraryId::Std if key.logical_path.as_slice() == ["std"] => "std".to_owned(), + LibraryId::Std => format!("std.{path}"), + LibraryId::External(name) => format!("@{name}.{path}"), + } +} diff --git a/crates/driver/src/paths.rs b/crates/driver/src/paths.rs new file mode 100644 index 00000000..cfb97053 --- /dev/null +++ b/crates/driver/src/paths.rs @@ -0,0 +1,240 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + env, fs, + path::{Component, Path, PathBuf}, +}; + +use hir::input::SourceFile; +use nameres::ModuleFsSnapshot; +use url::Url; + +use crate::{args::Args, db::DriverDb}; + +pub(crate) fn resolve_main_root(args: &Args, input_path: &Path) -> Result { + match &args.main_root { + Some(path) => { + absolutize(path).map_err(|err| format!("failed to resolve `{}`: {err}", path.display())) + } + None => Ok(input_path + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from("."))), + } +} + +pub(crate) fn resolve_std_root(args: &Args) -> Result { + if let Some(path) = &args.std_root { + let resolved = absolutize(path) + .map_err(|err| format!("failed to resolve `{}`: {err}", path.display()))?; + return validate_configured_std_root(resolved, "--std-root"); + } + if let Some(path) = env::var_os("SOLCORE_STD").filter(|value| !value.is_empty()) { + let path = PathBuf::from(path); + let resolved = absolutize(&path) + .map_err(|err| format!("failed to resolve `{}`: {err}", path.display()))?; + return validate_configured_std_root(resolved, "SOLCORE_STD"); + } + resolve_default_std_root(current_exe_std_root(), repo_root().join("std")) +} + +fn resolve_default_std_root( + exe_candidate: Option, + checkout_candidate: PathBuf, +) -> Result { + if let Some(path) = &exe_candidate + && path.is_dir() + { + return Ok(path.clone()); + } + if checkout_candidate.is_dir() { + return Ok(checkout_candidate); + } + let mut probed = Vec::new(); + if let Some(path) = exe_candidate { + probed.push(format!("`{}`", path.display())); + } + probed.push(format!("`{}`", checkout_candidate.display())); + Err(format!( + "could not locate the Solcore standard library; probed {}. Install the `std` directory next to the executable, pass --std-root DIR, or set SOLCORE_STD to an existing directory", + probed.join(" and ") + )) +} + +fn validate_configured_std_root(path: PathBuf, source: &str) -> Result { + if path.is_dir() { + return Ok(path); + } + let reason = if path.exists() { + "is not a directory" + } else { + "does not exist" + }; + Err(format!( + "Solcore standard library root from {source} {reason}: `{}`; pass --std-root DIR or set SOLCORE_STD to an existing directory", + path.display() + )) +} + +fn current_exe_std_root() -> Option { + let exe = env::current_exe().ok()?; + let dir = exe.parent()?; + Some(dir.join("std")) +} + +pub(crate) fn source_file_for_path( + db: &DriverDb, + path: &Path, + source: String, +) -> Result { + let url = Url::from_file_path(path) + .map_err(|()| format!("failed to convert `{}` into file URL", path.display()))?; + Ok(SourceFile::new(db, url, Some(source))) +} + +pub(crate) fn module_fs_snapshot_for_roots<'a>( + db: &DriverDb, + roots: impl IntoIterator, +) -> ModuleFsSnapshot { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +/// Converts a possibly relative path to a lexically normalized absolute path +/// without resolving symlinks. +pub(crate) fn absolutize(path: &Path) -> std::io::Result { + let absolute = if path.is_absolute() { + path.to_path_buf() + } else { + env::current_dir()?.join(path) + }; + Ok(normalize_lexically(&absolute)) +} + +fn normalize_lexically(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { + normalized.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => match normalized.components().next_back() { + Some(Component::Normal(_)) => { + normalized.pop(); + } + Some(Component::ParentDir) | None => normalized.push(".."), + Some(Component::Prefix(_) | Component::RootDir | Component::CurDir) => {} + }, + } + } + normalized +} + +/// Returns the repository root derived from the driver crate location. +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("driver crate lives under /crates/driver") + .to_path_buf() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lexical_normalization_removes_dot_and_parent_components() { + let normalized = normalize_lexically(Path::new("alpha/./beta/../gamma/main.solc")); + assert_eq!(normalized, PathBuf::from("alpha/gamma/main.solc")); + } + + #[test] + fn configured_std_root_must_be_an_existing_directory() { + let missing = + env::temp_dir().join(format!("solcore-missing-std-root-{}", std::process::id())); + + let error = validate_configured_std_root(missing.clone(), "--std-root") + .expect_err("missing std root should be rejected"); + + assert!(error.contains("does not exist"), "{error}"); + assert!(error.contains(&missing.display().to_string()), "{error}"); + assert!(error.contains("--std-root DIR"), "{error}"); + assert!(error.contains("SOLCORE_STD"), "{error}"); + } + + #[test] + fn file_next_to_executable_does_not_shadow_a_valid_std_directory() { + let root = env::temp_dir().join(format!("solcore-default-std-root-{}", std::process::id())); + let exe_candidate = root.join("bin-std"); + let checkout_candidate = root.join("checkout-std"); + fs::create_dir_all(&checkout_candidate).expect("create checkout std directory"); + fs::write(&exe_candidate, "not a directory").expect("write executable-adjacent file"); + + let resolved = resolve_default_std_root(Some(exe_candidate), checkout_candidate.clone()) + .expect("valid directory fallback should be selected"); + + assert_eq!(resolved, checkout_candidate); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn missing_default_std_roots_list_every_probe_and_remedy() { + let root = env::temp_dir().join(format!( + "solcore-missing-default-std-{}", + std::process::id() + )); + let exe_candidate = root.join("bin-std"); + let checkout_candidate = root.join("checkout-std"); + + let error = + resolve_default_std_root(Some(exe_candidate.clone()), checkout_candidate.clone()) + .expect_err("missing defaults should be rejected"); + + assert!( + error.contains(&exe_candidate.display().to_string()), + "{error}" + ); + assert!( + error.contains(&checkout_candidate.display().to_string()), + "{error}" + ); + assert!(error.contains("--std-root DIR"), "{error}"); + assert!(error.contains("SOLCORE_STD"), "{error}"); + } +} diff --git a/crates/driver/src/pipeline.rs b/crates/driver/src/pipeline.rs new file mode 100644 index 00000000..a31f043f --- /dev/null +++ b/crates/driver/src/pipeline.rs @@ -0,0 +1,177 @@ +use std::{collections::BTreeMap, env, ffi::OsString, fs}; + +use hir::diag::DiagnosticLevel; +use nameres::{LibraryId, ModuleTree, module_id_from_key, module_key_for_path}; + +use crate::{ + args::{ParsedArgs, help_text, parse_args, usage_text}, + db::DriverDb, + diagnostics::{apply_warning_policy, render_diagnostics}, + emit::{BackendFailure, maybe_emit_abi_outputs, maybe_emit_backend_outputs}, + modules::load_reachable_modules, + paths::{ + absolutize, module_fs_snapshot_for_roots, resolve_main_root, resolve_std_root, + source_file_for_path, + }, + trace::init_tracing, +}; + +pub(crate) fn run_compiler() { + let mut raw_args = env::args_os(); + let program = raw_args + .next() + .unwrap_or_else(|| OsString::from("solcore-driver")); + let program = program.to_string_lossy(); + let args = match parse_args(raw_args.collect()) { + Ok(ParsedArgs::Run(args)) => *args, + Ok(ParsedArgs::StandardJson) => { + crate::standard_json::run(); + return; + } + Ok(ParsedArgs::Help) => { + print!("{}", help_text(program.as_ref())); + return; + } + Ok(ParsedArgs::Version) => { + println!("Version: solcore-rs {}", env!("CARGO_PKG_VERSION")); + return; + } + Err(message) => { + eprintln!("{message}"); + eprintln!("{}", usage_text(program.as_ref())); + std::process::exit(2); + } + }; + init_tracing(args.trace); + + let input_path = match absolutize(&args.input) { + Ok(path) => path, + Err(err) => { + eprintln!("failed to resolve `{}`: {err}", args.input.display()); + std::process::exit(1); + } + }; + let source = match fs::read_to_string(&input_path) { + Ok(source) => source, + Err(err) => { + eprintln!("failed to read `{}`: {err}", input_path.display()); + std::process::exit(1); + } + }; + + let main_root = match resolve_main_root(&args, &input_path) { + Ok(path) => path, + Err(message) => { + eprintln!("{message}"); + std::process::exit(1); + } + }; + let std_root = match resolve_std_root(&args) { + Ok(path) => path, + Err(message) => { + eprintln!("{message}"); + std::process::exit(1); + } + }; + let external_roots = args + .external_roots + .iter() + .map(|(name, path)| { + absolutize(path) + .map(|path| (name.clone(), path)) + .map_err(|err| format!("failed to resolve `{}`: {err}", path.display())) + }) + .collect::, _>>(); + let external_roots = match external_roots { + Ok(roots) => roots, + Err(message) => { + eprintln!("{message}"); + std::process::exit(1); + } + }; + + let mut db = DriverDb::new(); + db.module_tree = Some(ModuleTree::new( + &db, + main_root.clone(), + std_root.clone(), + external_roots.clone(), + )); + let entry_key = match module_key_for_path(LibraryId::Main, &main_root, &input_path) { + Some(key) => key, + None => { + eprintln!( + "source file `{}` is outside module root `{}`", + input_path.display(), + main_root.display() + ); + std::process::exit(1); + } + }; + let entry_file = match source_file_for_path(&db, &input_path, source) { + Ok(file) => file, + Err(message) => { + eprintln!("{message}"); + std::process::exit(1); + } + }; + db.module_files.insert(entry_key.clone(), entry_file); + db.sync_module_file_snapshot(); + + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + &db, + std::iter::once(main_root.as_path()) + .chain(std::iter::once(std_root.as_path())) + .chain(external_roots.values().map(|path| path.as_path())), + )); + + if let Err(message) = load_reachable_modules(&mut db, entry_key.clone()) { + eprintln!("{message}"); + std::process::exit(1); + } + + let entry = module_id_from_key(&db, &entry_key); + let mut diagnostics = compiler::collect_frontend_diagnostics(&db, entry); + apply_warning_policy(&mut diagnostics, args.warning_policy); + let has_errors = diagnostics + .iter() + .any(|diagnostic| diagnostic.level == DiagnosticLevel::Error); + if !diagnostics.is_empty() { + eprint!("{}", render_diagnostics(&db, &diagnostics, &args)); + } + if !has_errors { + match maybe_emit_abi_outputs(&db, entry, &args) { + Ok(()) => {} + Err(message) => { + eprintln!("{message}"); + std::process::exit(1); + } + } + match maybe_emit_backend_outputs(&db, entry_file, &args) { + Ok(mut diagnostics) => { + apply_warning_policy(&mut diagnostics, args.warning_policy); + if !diagnostics.is_empty() { + eprint!("{}", render_diagnostics(&db, &diagnostics, &args)); + } + if diagnostics + .iter() + .any(|diagnostic| diagnostic.level == DiagnosticLevel::Error) + { + std::process::exit(1); + } + } + Err(BackendFailure::Diagnostics(mut diagnostics)) => { + apply_warning_policy(&mut diagnostics, args.warning_policy); + eprint!("{}", render_diagnostics(&db, &diagnostics, &args)); + std::process::exit(1); + } + Err(BackendFailure::Message(message)) => { + eprintln!("{message}"); + std::process::exit(1); + } + } + return; + } + + std::process::exit(1); +} diff --git a/crates/driver/src/standard_json.rs b/crates/driver/src/standard_json.rs new file mode 100644 index 00000000..2d5b652d --- /dev/null +++ b/crates/driver/src/standard_json.rs @@ -0,0 +1,314 @@ +//! Solcore Standard JSON adapter used by external compiler benchmarks. +//! +//! The adapter deliberately accepts the small, stable subset that both +//! Solcore implementations can share: virtual source files, an entrypoint, +//! and either frontend-only or checked-Hull compilation. Solidity-specific +//! settings injected by `solc-bench` are ignored rather than rejected. + +use std::{ + collections::BTreeMap, + io::{self, Read, Write}, + path::{Component, Path}, +}; + +use serde_json::{Map, Value, json}; +use vfs::{Diagnostic, DiagnosticSeverity, Workspace, WorkspaceFileChange}; + +const DEFAULT_ENTRYPOINT: &str = "main.solc"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Stage { + Frontend, + Hull, +} + +struct Request { + sources: BTreeMap, + entrypoint: String, + stage: Stage, +} + +/// Reads one request from stdin and writes exactly one JSON response to stdout. +/// +/// Input errors and ordinary compiler diagnostics deliberately stay in the +/// response and exit successfully. This matches the `solc --standard-json` +/// process contract, so a benchmark runner can distinguish an invalid test +/// case from a failed compiler process. +pub(crate) fn run() { + let response = + run_inner().unwrap_or_else(|message| response_with_errors(vec![request_error(message)])); + let stdout = io::stdout(); + let mut stdout = io::BufWriter::new(stdout.lock()); + let _ = serde_json::to_writer(&mut stdout, &response); + let _ = stdout.write_all(b"\n"); +} + +fn run_inner() -> Result { + let mut input = Vec::new(); + io::stdin() + .read_to_end(&mut input) + .map_err(|error| format!("failed to read Standard JSON input: {error}"))?; + let request = serde_json::from_slice(&input) + .map_err(|error| format!("invalid Standard JSON input: {error}"))?; + let request = parse_request(request)?; + Ok(response_with_errors(compile_request(request)?)) +} + +fn parse_request(input: Value) -> Result { + let root = input + .as_object() + .ok_or_else(|| "Standard JSON input must be an object".to_owned())?; + let language = root + .get("language") + .and_then(Value::as_str) + .ok_or_else(|| "Standard JSON input requires string field `language`".to_owned())?; + if language != "Solcore" { + return Err(format!( + "unsupported language `{language}`; expected `Solcore`" + )); + } + + let sources_value = root + .get("sources") + .ok_or_else(|| "Standard JSON input requires object field `sources`".to_owned())?; + let sources_object = sources_value + .as_object() + .ok_or_else(|| "Standard JSON field `sources` must be an object".to_owned())?; + if sources_object.is_empty() { + return Err("Standard JSON field `sources` must not be empty".to_owned()); + } + + let mut sources = BTreeMap::new(); + for (name, source) in sources_object { + validate_source_name(name)?; + let source = source + .as_object() + .ok_or_else(|| format!("source `{name}` must be an object"))?; + let content = source + .get("content") + .and_then(Value::as_str) + .ok_or_else(|| format!("source `{name}` requires string field `content`"))?; + sources.insert(name.clone(), content.to_owned()); + } + + let settings = match root.get("settings") { + None => None, + Some(Value::Object(settings)) => Some(settings), + Some(_) => return Err("Standard JSON field `settings` must be an object".to_owned()), + }; + let solcore_settings = match settings.and_then(|settings| settings.get("solcore")) { + None => None, + Some(Value::Object(settings)) => Some(settings), + Some(_) => { + return Err("Standard JSON field `settings.solcore` must be an object".to_owned()); + } + }; + + let entrypoint = match solcore_settings.and_then(|settings| settings.get("entrypoint")) { + None => sources + .contains_key(DEFAULT_ENTRYPOINT) + .then(|| DEFAULT_ENTRYPOINT.to_owned()) + .or_else(|| sources.keys().next().cloned()) + .expect("sources is non-empty"), + Some(Value::String(entrypoint)) => entrypoint.clone(), + Some(_) => { + return Err( + "Standard JSON field `settings.solcore.entrypoint` must be a string".to_owned(), + ); + } + }; + validate_source_name(&entrypoint)?; + if !sources.contains_key(&entrypoint) { + return Err(format!( + "Standard JSON entrypoint `{entrypoint}` is not present in `sources`" + )); + } + + let stage = match solcore_settings.and_then(|settings| settings.get("stage")) { + None => Stage::Hull, + Some(Value::String(stage)) if stage == "hull" => Stage::Hull, + Some(Value::String(stage)) if stage == "frontend" => Stage::Frontend, + Some(Value::String(stage)) => { + return Err(format!( + "unsupported `settings.solcore.stage` `{stage}`; expected `frontend` or `hull`" + )); + } + Some(_) => { + return Err("Standard JSON field `settings.solcore.stage` must be a string".to_owned()); + } + }; + + Ok(Request { + sources, + entrypoint, + stage, + }) +} + +fn validate_source_name(name: &str) -> Result<(), String> { + let path = Path::new(name); + let has_only_normal_components = path + .components() + .all(|component| matches!(component, Component::Normal(_))); + if name.is_empty() + || name.contains('\\') + || name.contains(':') + || !has_only_normal_components + || path.extension().and_then(|extension| extension.to_str()) != Some("solc") + { + return Err(format!( + "source name `{name}` must be a relative, traversal-free `.solc` path" + )); + } + Ok(()) +} + +fn compile_request(request: Request) -> Result, String> { + let mut workspace = Workspace::new(); + workspace.apply_file_changes( + request + .sources + .into_iter() + .map(|(path, contents)| WorkspaceFileChange::Set { path, contents }), + ); + workspace.set_entry(&request.entrypoint); + + let mut diagnostics = workspace + .raw_diagnostics() + .into_iter() + .map(|diagnostic| Diagnostic::from_hir(workspace.db(), diagnostic)) + .collect::>(); + + if request.stage == Stage::Hull && !has_errors(&diagnostics) { + let entry_path = Path::new(vfs::MAIN_ROOT).join(&request.entrypoint); + let entry_file = workspace + .db() + .source_file(entry_path) + .ok_or_else(|| "Standard JSON entrypoint was not loaded into the VFS".to_owned())?; + match compiler::build_checked_hull( + workspace.db(), + entry_file, + specialize::SpecializeOptions::default(), + ) { + Ok(checked) => diagnostics.extend( + checked + .diagnostics + .into_iter() + .map(|diagnostic| Diagnostic::from_hir(workspace.db(), diagnostic)), + ), + Err(stage_diagnostics) => diagnostics.extend( + stage_diagnostics + .into_iter() + .map(|diagnostic| Diagnostic::from_hir(workspace.db(), diagnostic)), + ), + } + } + + Ok(diagnostics.iter().map(compiler_diagnostic).collect()) +} + +fn has_errors(diagnostics: &[Diagnostic]) -> bool { + diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error) +} + +fn compiler_diagnostic(diagnostic: &Diagnostic) -> Value { + let severity = match diagnostic.severity { + DiagnosticSeverity::Error => "error", + DiagnosticSeverity::Warning => "warning", + DiagnosticSeverity::Note | DiagnosticSeverity::Help => "info", + }; + let code = diagnostic.code.clone(); + let formatted_message = match (&diagnostic.primary, code.as_deref()) { + (Some(primary), Some(code)) => format!( + "{}:{}-{}: {severity}[{code}]: {}\n", + source_name(&primary.file_url), + primary.start, + primary.end, + diagnostic.message + ), + (Some(primary), None) => format!( + "{}:{}-{}: {severity}: {}\n", + source_name(&primary.file_url), + primary.start, + primary.end, + diagnostic.message + ), + (None, Some(code)) => format!("{severity}[{code}]: {}\n", diagnostic.message), + (None, None) => format!("{severity}: {}\n", diagnostic.message), + }; + let mut result = Map::new(); + result.insert( + "component".to_owned(), + Value::String("solcore-rs".to_owned()), + ); + result.insert("severity".to_owned(), Value::String(severity.to_owned())); + result.insert( + "message".to_owned(), + Value::String(diagnostic.message.clone()), + ); + result.insert( + "formattedMessage".to_owned(), + Value::String(formatted_message), + ); + if let Some(code) = code { + result.insert("type".to_owned(), Value::String(code)); + } + if let Some(primary) = &diagnostic.primary { + result.insert( + "sourceLocation".to_owned(), + json!({ + "file": source_name(&primary.file_url), + "start": primary.start, + "end": primary.end, + }), + ); + } + Value::Object(result) +} + +fn source_name(file_url: &str) -> String { + file_url + .strip_prefix("file:///main/") + .unwrap_or(file_url) + .to_owned() +} + +fn request_error(message: String) -> Value { + json!({ + "component": "solcore-rs", + "severity": "error", + "type": "StandardJsonError", + "message": message, + "formattedMessage": format!("error: {message}\n"), + }) +} + +fn response_with_errors(errors: Vec) -> Value { + json!({ "errors": errors }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_source_paths_that_escape_the_virtual_workspace() { + for source_name in ["../main.solc", "/main.solc", "dir\\main.solc", "main.sol"] { + assert!(validate_source_name(source_name).is_err(), "{source_name}"); + } + } + + #[test] + fn defaults_to_main_entrypoint_and_hull_stage() { + let request = parse_request(json!({ + "language": "Solcore", + "sources": {"main.solc": {"content": "function main() returns (word) { return 0; }"}}, + })) + .expect("valid request"); + + assert_eq!(request.entrypoint, "main.solc"); + assert_eq!(request.stage, Stage::Hull); + } +} diff --git a/crates/driver/src/trace.rs b/crates/driver/src/trace.rs new file mode 100644 index 00000000..8815e16b --- /dev/null +++ b/crates/driver/src/trace.rs @@ -0,0 +1,100 @@ +use std::env; + +use tracing_subscriber::EnvFilter; + +const TRACE_DEFAULT_FILTER: &str = concat!( + "warn,", + "driver::modules=debug,", + "compiler::pipeline=debug,compiler::abi=debug,", + "parser=debug,parser::query=debug,parser::recovery=trace,", + "hir::query=debug,", + "hir_ty::frontend=debug,hir_ty::query=debug,", + "nameres=debug,nameres::query=debug,nameres::imports=trace,nameres::fixpoint=debug,", + "driver::salsa=debug" +); + +pub(crate) fn init_tracing(trace: bool) { + let has_rust_log = env::var_os("RUST_LOG").is_some(); + if !trace && !has_rust_log { + return; + } + + let filter = if has_rust_log { + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(TRACE_DEFAULT_FILTER)) + } else { + EnvFilter::new(TRACE_DEFAULT_FILTER) + }; + + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_writer(std::io::stderr) + .compact() + .init(); +} + +pub(crate) fn emit_salsa_event(event: salsa::Event) { + match event.kind { + salsa::EventKind::WillExecute { database_key } => { + tracing::debug!( + target: "driver::salsa", + event = "WillExecute", + thread = ?event.thread_id, + key = ?database_key, + "salsa query will execute" + ); + } + salsa::EventKind::DidValidateMemoizedValue { database_key } => { + tracing::debug!( + target: "driver::salsa", + event = "DidValidateMemoizedValue", + thread = ?event.thread_id, + key = ?database_key, + "salsa memoized value validated" + ); + } + salsa::EventKind::DidValidateInternedValue { key, revision } => { + tracing::trace!( + target: "driver::salsa", + event = "DidValidateInternedValue", + thread = ?event.thread_id, + key = ?key, + revision = ?revision, + "salsa interned value validated" + ); + } + salsa::EventKind::WillIterateCycle { + database_key, + iteration, + } => { + tracing::debug!( + target: "driver::salsa", + event = "WillIterateCycle", + thread = ?event.thread_id, + key = ?database_key, + iteration, + "salsa cycle will iterate" + ); + } + salsa::EventKind::DidFinalizeCycle { + database_key, + iteration, + } => { + tracing::debug!( + target: "driver::salsa", + event = "DidFinalizeCycle", + thread = ?event.thread_id, + key = ?database_key, + iteration, + "salsa cycle finalized" + ); + } + kind => { + tracing::trace!( + target: "driver::salsa", + thread = ?event.thread_id, + kind = ?kind, + "salsa event" + ); + } + } +} diff --git a/crates/driver/tests/standard_json_cli.rs b/crates/driver/tests/standard_json_cli.rs new file mode 100644 index 00000000..f9220288 --- /dev/null +++ b/crates/driver/tests/standard_json_cli.rs @@ -0,0 +1,91 @@ +use std::{ + io::Write, + process::{Command, Output, Stdio}, +}; + +use serde_json::{Value, json}; + +fn run_standard_json(input: Value) -> Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--standard-json") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn standard JSON driver"); + let encoded = serde_json::to_vec(&input).expect("serialize request"); + child + .stdin + .take() + .expect("piped stdin") + .write_all(&encoded) + .expect("write request"); + child + .wait_with_output() + .expect("wait for standard JSON driver") +} + +fn response(output: &Output) -> Value { + assert!( + output.status.success(), + "stderr:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap_or_else(|error| { + panic!( + "stdout is not JSON: {error}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + }) +} + +fn has_error(response: &Value) -> bool { + response["errors"] + .as_array() + .expect("response errors array") + .iter() + .any(|error| error["severity"] == "error") +} + +#[test] +fn standard_json_compiles_checked_hull_without_polluting_stdout() { + let output = run_standard_json(json!({ + "language": "Solcore", + "sources": { + "main.solc": {"content": "function id(x: word) returns (word) { return x; }\n"} + }, + "settings": {"solcore": {"entrypoint": "main.solc", "stage": "hull"}}, + })); + let response = response(&output); + + assert!(!has_error(&response), "response: {response:#}"); + assert!(String::from_utf8_lossy(&output.stderr).is_empty()); +} + +#[test] +fn standard_json_loads_multiple_virtual_source_files() { + let output = run_standard_json(json!({ + "language": "Solcore", + "sources": { + "main.solc": {"content": "import {id} from helper;\nfunction main() returns (word) { return id(0); }\n"}, + "helper.solc": {"content": "export { id };\nfunction id(x: word) returns (word) { return x; }\n"}, + }, + "settings": {"solcore": {"entrypoint": "main.solc", "stage": "frontend"}}, + })); + let response = response(&output); + + assert!(!has_error(&response), "response: {response:#}"); +} + +#[test] +fn standard_json_reports_request_errors_in_json() { + let output = run_standard_json(json!({ + "language": "Solcore", + "sources": {"../escape.solc": {"content": "function main() returns (word) { return 0; }"}}, + })); + let response = response(&output); + + assert!(has_error(&response), "response: {response:#}"); + assert_eq!(response["errors"][0]["type"], "StandardJsonError"); +} diff --git a/crates/driver/tests/typeck_cli.rs b/crates/driver/tests/typeck_cli.rs new file mode 100644 index 00000000..27e6d045 --- /dev/null +++ b/crates/driver/tests/typeck_cli.rs @@ -0,0 +1,1061 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[test] +fn cli_prints_help_and_version() { + let help = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--help") + .output() + .expect("run driver help"); + assert!(help.status.success(), "help failed"); + let stdout = String::from_utf8_lossy(&help.stdout); + assert!(stdout.contains("-f, --file FILE"), "{stdout}"); + assert!(stdout.contains("--std-root DIR"), "{stdout}"); + assert!(stdout.contains("--color auto|always|never"), "{stdout}"); + assert!(stdout.contains("--unicode auto|always|never"), "{stdout}"); + assert!(stdout.contains("--diagnostic-width N"), "{stdout}"); + assert!( + stdout.contains("--diagnostic-format human|short"), + "{stdout}" + ); + assert!( + stdout.contains("--warnings default|always|never|deny"), + "{stdout}" + ); + assert!(stdout.contains("-o, --output-dir DIR"), "{stdout}"); + assert!(stdout.contains("--abi"), "{stdout}"); + assert!(stdout.contains("--emit-sonatina[=FILE]"), "{stdout}"); + assert!(stdout.contains("--pe-fuel N"), "{stdout}"); + assert!(stdout.contains("--pe-depth N"), "{stdout}"); + assert!(stdout.contains("--pe-max-instantiations N"), "{stdout}"); + assert!(stdout.contains("--pe-max-type-nodes N"), "{stdout}"); + assert!(stdout.contains("--root DIR"), "{stdout}"); + + let version = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--version") + .output() + .expect("run driver version"); + assert!(version.status.success(), "version failed"); + assert_eq!( + String::from_utf8_lossy(&version.stdout), + format!("Version: solcore-rs {}\n", env!("CARGO_PKG_VERSION")) + ); +} + +#[test] +fn cli_reports_usage_errors_with_exit_code_2() { + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--definitely-not-a-real-flag") + .output() + .expect("run driver usage error"); + + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("unknown option"), "{stderr}"); + assert!(stderr.contains("--help"), "{stderr}"); +} + +#[test] +fn cli_trace_reports_pipeline_summaries_without_verbose_intern_events() { + let dir = temp_dir("trace-pipeline"); + let output_dir = dir.join("artifacts"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write( + &input, + "contract C { function main() public returns (word) { return 42; } }\n", + ) + .expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--trace") + .arg("--abi") + .arg("--emit-hull=main.hull") + .arg("--output-dir") + .arg(&output_dir) + .arg(&input) + .env_remove("RUST_LOG") + .output() + .expect("run traced driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!( + output.status.success(), + "traced driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + for summary in [ + "reachable module loading completed", + "frontend diagnostics collected", + "contract ABI collection completed", + "compiler stage completed", + "backend pipeline completed", + ] { + assert!(stderr.contains(summary), "missing `{summary}`:\n{stderr}"); + } + assert!( + !stderr.contains("DidValidateInternedValue"), + "default trace should omit high-volume intern validation events:\n{stderr}" + ); + assert!( + !stderr.contains("salsa::function::execute"), + "default trace should not enable Salsa's internal target:\n{stderr}" + ); +} + +#[test] +fn cli_prints_typeck_mismatch_diagnostic() { + let stderr = driver_stderr( + "mismatch", + "function main() returns (word) { return true; }\n", + ); + + assert!(stderr.contains("error[SC0201]"), "stderr:\n{stderr}"); + assert_eq!( + stderr.matches("error[SC0201]").count(), + 1, + "expected one SC0201 diagnostic:\n{stderr}" + ); + assert!( + stderr.contains("1 | function main() returns (word) { return true; }"), + "expected source line in stderr:\n{stderr}" + ); + assert!( + stderr.contains("^^^^ expression has mismatched type"), + "expected caret label in stderr:\n{stderr}" + ); +} + +#[test] +fn cli_prints_short_diagnostics() { + let dir = temp_dir("short-diagnostic"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write(&input, "function main() returns (word) { return true; }\n").expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--color=never") + .arg("--diagnostic-format=short") + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("main.solc:1:41: error[SC0201]: type mismatch: expected word, found bool"), + "stderr:\n{stderr}" + ); + assert!( + !stderr.contains("function main()"), + "short output should not include source snippets:\n{stderr}" + ); +} + +#[cfg(unix)] +#[test] +fn cli_reports_non_utf8_input_path_without_panic() { + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + + let dir = temp_dir("non-utf8-arg"); + fs::create_dir_all(&dir).expect("create temp dir"); + let root = dir.clone(); + let mut raw = dir.into_os_string().into_vec(); + raw.extend_from_slice(b"/bad-\xff.solc"); + let input = OsString::from_vec(raw); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg(input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&root); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("failed to read"), "stderr:\n{stderr}"); + assert!(!stderr.contains("panicked"), "stderr:\n{stderr}"); + assert!( + !stderr.contains("thread 'solcore-compiler'"), + "stderr:\n{stderr}" + ); +} + +#[test] +fn cli_reports_reachable_missing_external_lib_root() { + let dir = temp_dir("missing-external-root"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let missing = dir.join("missing-ext"); + fs::write( + &input, + "import * as util from @pkg.util;\nfunction main() returns (word) { return 0; }\n", + ) + .expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--color=never") + .arg("--external-lib") + .arg(format!("pkg={}", missing.display())) + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("external library `@pkg` root directory does not exist"), + "stderr:\n{stderr}" + ); + assert!( + stderr.contains(&missing.display().to_string()), + "stderr:\n{stderr}" + ); + assert!( + stderr.contains("note: pass --external-lib pkg=PATH with an existing directory"), + "stderr:\n{stderr}" + ); +} + +#[test] +fn cli_reports_unreadable_reachable_module_as_io_error() { + let dir = temp_dir("invalid-utf8-module"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let dependency = dir.join("util.solc"); + fs::write( + &input, + "import * as util from util;\nfunction main() returns (word) { return 0; }\n", + ) + .expect("write source"); + fs::write(&dependency, [0xff, 0xfe]).expect("write invalid UTF-8 dependency"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--color=never") + .arg("--root") + .arg(&dir) + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("failed to read module `util`") + && stderr.contains(&dependency.display().to_string()) + && stderr.contains("valid UTF-8"), + "stderr:\n{stderr}" + ); + assert!( + !stderr.contains("file not found") && !stderr.contains("did you mean"), + "stderr should report the I/O cause, not a missing module:\n{stderr}" + ); +} + +#[test] +fn cli_accepts_warning_policy_and_diagnostic_rendering_flags() { + let dir = temp_dir("warning-policy"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write(&input, "function main() returns (word) { return 0; }\n").expect("write source"); + + for policy in ["default", "always", "never", "deny"] { + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg(format!("--warnings={policy}")) + .arg("--unicode=never") + .arg("--diagnostic-width=40") + .arg(&input) + .output() + .expect("run driver"); + assert!( + output.status.success(), + "driver failed for --warnings={policy}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + let file_flag = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--file") + .arg(&input) + .output() + .expect("run driver"); + assert!( + file_flag.status.success(), + "driver failed for --file\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&file_flag.stdout), + String::from_utf8_lossy(&file_flag.stderr) + ); + + let invalid = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--warnings=loud") + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(invalid.status.code(), Some(2)); + let stderr = String::from_utf8_lossy(&invalid.stderr); + assert!( + stderr.contains("--warnings must be one of"), + "stderr:\n{stderr}" + ); +} + +#[test] +fn cli_warning_policy_default_prints_warnings() { + let dir = temp_dir("warning-policy-output"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write( + &input, + r#"enum Flag { Off, On } + +function pick(x: Flag) returns (word) { + match (x) { + default { return 0; } + case Flag.Off { return 1; } + } +} +"#, + ) + .expect("write source"); + + let default = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--unicode=never") + .arg("--diagnostic-format=short") + .arg(&input) + .output() + .expect("run driver"); + assert!( + default.status.success(), + "default warning policy failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&default.stdout), + String::from_utf8_lossy(&default.stderr) + ); + let stderr = String::from_utf8_lossy(&default.stderr); + assert!(stderr.contains("warning[SC0303]"), "stderr:\n{stderr}"); + + let never = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--warnings=never") + .arg("--unicode=never") + .arg("--diagnostic-format=short") + .arg(&input) + .output() + .expect("run driver"); + assert!( + never.status.success(), + "never warning policy failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&never.stdout), + String::from_utf8_lossy(&never.stderr) + ); + let stderr = String::from_utf8_lossy(&never.stderr); + assert!( + !stderr.contains("warning[SC0303]"), + "stderr should not contain warnings:\n{stderr}" + ); + + let deny = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--warnings=deny") + .arg("--unicode=never") + .arg("--diagnostic-format=short") + .arg(&input) + .output() + .expect("run driver"); + assert_eq!(deny.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&deny.stderr); + assert!(stderr.contains("error[SC0303]"), "stderr:\n{stderr}"); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_prints_solver_diagnostic_with_obligation_span() { + let stderr = driver_stderr( + "solver", + r#"trait C {} +function use(x: a) returns (word) where a: C { return 0; } +function main(x: word) returns (word) { return use(x); } +"#, + ); + + assert!(stderr.contains("error[SC0207]"), "stderr:\n{stderr}"); + assert!( + stderr.contains("3 | function main(x: word) returns (word) { return use(x); }"), + "expected source line in stderr:\n{stderr}" + ); + assert!( + stderr.contains("^^^^^^ constraint originates here"), + "expected solver caret label in stderr:\n{stderr}" + ); +} + +#[test] +fn cli_prints_instance_soundness_diagnostic_with_head_span() { + let stderr = driver_stderr( + "instance-soundness", + r#"enum Box { Box(word) } +trait MyClass {} +impl MyClass, b> {} +"#, + ); + + assert!(stderr.contains("error[SC0212]"), "stderr:\n{stderr}"); + assert!( + stderr.contains("3 | impl MyClass, b> {}"), + "expected impl source line in stderr:\n{stderr}" + ); + assert!( + stderr.contains("impl head does not determine these variables"), + "expected impl head caret label in stderr:\n{stderr}" + ); +} + +#[test] +fn cli_uses_root_override_for_main_library() { + let dir = temp_dir("root-override"); + let nested = dir.join("nested"); + fs::create_dir_all(&nested).expect("create temp dirs"); + fs::write( + dir.join("lib.solc"), + "export { value };\nfunction value() returns (word) { return 5; }\n", + ) + .expect("write lib"); + let input = nested.join("main.solc"); + fs::write( + &input, + "import * as lib from lib.lib;\nfunction main() returns (word) { return lib.value(); }\n", + ) + .expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--root") + .arg(&dir) + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!( + output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn cli_uses_explicit_std_root() { + let dir = temp_dir("explicit-std-root"); + let std_root = dir.join("custom-std"); + let input_dir = dir.join("src"); + fs::create_dir_all(&std_root).expect("create std dir"); + fs::create_dir_all(&input_dir).expect("create input dir"); + write_fake_std(&std_root); + let input = input_dir.join("main.solc"); + write_fake_std_importer(&input); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--std-root") + .arg(&std_root) + .arg(&input) + .env_remove("SOLCORE_STD") + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!( + output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn cli_rejects_missing_std_root_with_actionable_configuration_help() { + let dir = temp_dir("missing-std-root"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let missing = dir.join("missing-std"); + fs::write(&input, "function main() returns (word) { return 0; }\n").expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--std-root") + .arg(&missing) + .arg(&input) + .env_remove("SOLCORE_STD") + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("standard library root"), + "stderr:\n{stderr}" + ); + assert!(stderr.contains("does not exist"), "stderr:\n{stderr}"); + assert!( + stderr.contains(&missing.display().to_string()), + "stderr:\n{stderr}" + ); + assert!(stderr.contains("--std-root DIR"), "stderr:\n{stderr}"); + assert!(stderr.contains("SOLCORE_STD"), "stderr:\n{stderr}"); +} + +#[test] +fn cli_normalizes_parent_components_before_deriving_the_entry_module() { + let dir = temp_dir("normalized-entry-path"); + let src = dir.join("src"); + fs::create_dir_all(&src).expect("create source directory"); + fs::write( + src.join("util.solc"), + "export { value }; function value() returns (word) { return 9; }\n", + ) + .expect("write utility module"); + let input = src.join("main.solc"); + fs::write( + &input, + "import * as util from util; function main() returns (word) { return util.value(); }\n", + ) + .expect("write source"); + let spelled_with_parent = src.join("..").join("src").join("main.solc"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--root") + .arg(&dir) + .arg(&spelled_with_parent) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!( + output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn copied_binary_resolves_std_next_to_current_exe() { + let dir = temp_dir("copied-binary-std"); + let input_dir = dir.join("src"); + fs::create_dir_all(&input_dir).expect("create input dir"); + let copied_driver = dir.join("solcore-driver"); + fs::copy(env!("CARGO_BIN_EXE_solcore-driver"), &copied_driver).expect("copy driver"); + write_fake_std(&dir.join("std")); + let input = input_dir.join("main.solc"); + write_fake_std_importer(&input); + + let output = Command::new(&copied_driver) + .arg(&input) + .env_remove("SOLCORE_STD") + .output() + .expect("run copied driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!( + output.status.success(), + "copied driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn cli_emits_yul_to_stdout_and_hull_to_file() { + let dir = temp_dir("emit-backends"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let output_dir = dir.join("artifacts"); + let hull_output = output_dir.join("main.hull"); + fs::write( + &input, + r#" +contract C { + function main() public returns (word) { + return 42; + } +} +"#, + ) + .expect("write source"); + + let yul = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--emit-yul") + .arg(&input) + .output() + .expect("run driver yul"); + assert!( + yul.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&yul.stdout), + String::from_utf8_lossy(&yul.stderr) + ); + let yul_stdout = String::from_utf8_lossy(&yul.stdout); + assert!(yul_stdout.contains("object \"CDeploy\""), "{yul_stdout}"); + assert!( + yul_stdout.contains("function usr$main_C_main_"), + "{yul_stdout}" + ); + assert!(!yul_stdout.contains("dispatch_selector"), "{yul_stdout}"); + + let hull = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--output-dir") + .arg(&output_dir) + .arg("--emit-hull=main.hull") + .arg(&input) + .output() + .expect("run driver hull"); + assert!( + hull.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&hull.stdout), + String::from_utf8_lossy(&hull.stderr) + ); + let hull_text = fs::read_to_string(&hull_output).expect("read hull output"); + assert!(hull_text.contains("object \"CDeploy\""), "{hull_text}"); + assert!(hull_text.contains("function main_C_main_"), "{hull_text}"); + assert!(!hull_text.contains("dispatch_selector"), "{hull_text}"); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_emits_sonatina_to_stdout_and_output_dir() { + let dir = temp_dir("emit-sonatina"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let output_dir = dir.join("artifacts"); + let sonatina_output = output_dir.join("main.sonatina"); + fs::write(&input, "function main() returns (word) { return 42; }\n").expect("write source"); + + let stdout_output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--emit-sonatina") + .arg(&input) + .output() + .expect("run driver Sonatina stdout"); + assert!( + stdout_output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&stdout_output.stdout), + String::from_utf8_lossy(&stdout_output.stderr) + ); + let sonatina_stdout = + String::from_utf8(stdout_output.stdout).expect("Sonatina output is UTF-8"); + assert!(!sonatina_stdout.trim().is_empty(), "empty Sonatina output"); + + let file_output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--output-dir") + .arg(&output_dir) + .arg("--emit-sonatina=main.sonatina") + .arg(&input) + .output() + .expect("run driver Sonatina file output"); + assert!( + file_output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&file_output.stdout), + String::from_utf8_lossy(&file_output.stderr) + ); + assert!(file_output.stdout.is_empty(), "unexpected stdout"); + let sonatina_file = fs::read_to_string(&sonatina_output).expect("read Sonatina output"); + assert_eq!(sonatina_file, sonatina_stdout); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_rejects_multiple_backend_stdout_targets() { + let dir = temp_dir("multiple-backend-stdout"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write(&input, "function main() returns (word) { return 42; }\n").expect("write source"); + + for (first, second) in [ + ("--emit-hull", "--emit-yul"), + ("--emit-hull", "--emit-sonatina"), + ("--emit-yul", "--emit-sonatina"), + ] { + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg(first) + .arg(second) + .arg(&input) + .output() + .expect("run driver with conflicting stdout outputs"); + assert_eq!( + output.status.code(), + Some(1), + "{first} + {second} unexpectedly succeeded" + ); + let stderr = strip_ansi(&String::from_utf8_lossy(&output.stderr)); + assert!( + stderr.contains("cannot write multiple backend outputs to stdout"), + "stderr:\n{stderr}" + ); + assert!(stderr.contains(first), "stderr:\n{stderr}"); + assert!(stderr.contains(second), "stderr:\n{stderr}"); + } + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_emits_abi_to_output_dir() { + let dir = temp_dir("emit-abi"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + let output_dir = dir.join("abi"); + let abi_output = output_dir.join("C.abi"); + fs::write( + &input, + r#" +contract C { + function main() public returns (word) { + return 42; + } +} +"#, + ) + .expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--abi") + .arg("-o") + .arg(&output_dir) + .arg(&input) + .output() + .expect("run driver"); + assert!( + output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let abi = fs::read_to_string(&abi_output).expect("read ABI output"); + assert!(abi.contains("\"name\": \"main\""), "{abi}"); + assert!(abi.contains("\"type\": \"function\""), "{abi}"); + assert!(abi.contains("\"type\": \"uint256\""), "{abi}"); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_abi_ignores_reachable_external_library_contracts() { + let dir = temp_dir("abi-external-scope"); + let external = dir.join("external"); + let output_dir = dir.join("abi"); + fs::create_dir_all(&external).expect("create external root"); + fs::write( + external.join("token.solc"), + "contract ExternalToken { function main() public returns (word) { return 7; } }\n", + ) + .expect("write external module"); + let input = dir.join("main.solc"); + fs::write( + &input, + "import * as token from @pkg.token; contract Local { function main() public returns (word) { return 1; } }\n", + ) + .expect("write main module"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--abi") + .arg("--output-dir") + .arg(&output_dir) + .arg("--external-lib") + .arg(format!("pkg={}", external.display())) + .arg(&input) + .output() + .expect("run driver"); + + assert!( + output.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output_dir.join("Local.abi").is_file()); + assert!(!output_dir.join("ExternalToken.abi").exists()); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_abi_rejects_colliding_local_contract_filenames_before_writing() { + let dir = temp_dir("abi-local-collision"); + let output_dir = dir.join("abi"); + fs::create_dir_all(&dir).expect("create temp dir"); + fs::write( + dir.join("a.solc"), + "contract Token { function main() public returns (word) { return 1; } }\n", + ) + .expect("write first module"); + fs::write( + dir.join("b.solc"), + "contract Token { function main() public returns (word) { return 2; } }\n", + ) + .expect("write second module"); + let input = dir.join("main.solc"); + fs::write( + &input, + "import * as a from a; import * as b from b; function main() returns (word) { return 0; }\n", + ) + .expect("write main module"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--abi") + .arg("--output-dir") + .arg(&output_dir) + .arg(&input) + .output() + .expect("run driver"); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("cannot emit `Token.abi`"), + "stderr:\n{stderr}" + ); + assert!(stderr.contains("both"), "stderr:\n{stderr}"); + assert!(stderr.contains("`a`"), "stderr:\n{stderr}"); + assert!(stderr.contains("`b`"), "stderr:\n{stderr}"); + assert!(!output_dir.join("Token.abi").exists()); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_renders_backend_diagnostics_with_stable_codes() { + let dir = temp_dir("backend-diagnostic"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write( + &input, + r#" +import {string} from std; +contract C { + function main() public returns (string) { + return "nope"; + } +} +"#, + ) + .expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--emit-hull") + .arg("--color=never") + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert_eq!(output.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("error[SC0421]"), "stderr:\n{stderr}"); + assert!( + stderr.contains("cannot lower literal `\"nope\"` to Hull"), + "stderr:\n{stderr}" + ); + assert!(!stderr.contains("UnsupportedType {"), "stderr:\n{stderr}"); + assert!(!stderr.contains("HULL-EMIT"), "stderr:\n{stderr}"); +} + +#[test] +fn cli_partial_evaluation_fuel_is_configurable() { + let dir = temp_dir("configurable-pe-fuel"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write( + &input, + r#" +import std; +function g2() returns (word) { return 1; } +function g1() returns (word) { return g2() + g2(); } +function g0() returns (word) { return g1() + g1(); } +contract C { function main() returns (word) { return g0(); } } +"#, + ) + .expect("write source"); + + let exhausted = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--pe-fuel=3") + .arg("--emit-hull") + .arg("--color=never") + .arg(&input) + .output() + .expect("run low-fuel driver"); + assert_eq!(exhausted.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&exhausted.stderr); + assert!(stderr.contains("fuel exhausted"), "stderr:\n{stderr}"); + assert!(stderr.contains("--pe-fuel"), "stderr:\n{stderr}"); + + let enough = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--pe-fuel=64") + .arg("--emit-hull") + .arg(&input) + .output() + .expect("run higher-fuel driver"); + assert!( + enough.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&enough.stdout), + String::from_utf8_lossy(&enough.stderr) + ); + + let invalid = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--pe-depth=0") + .arg(&input) + .output() + .expect("run invalid resource limit"); + assert_eq!(invalid.status.code(), Some(2)); + assert!( + String::from_utf8_lossy(&invalid.stderr).contains("positive integer"), + "stderr:\n{}", + String::from_utf8_lossy(&invalid.stderr) + ); + + let _ = fs::remove_dir_all(&dir); +} + +#[test] +fn cli_emit_yul_requires_one_top_level_object_or_selection() { + let dir = temp_dir("emit-yul-multi-object"); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write( + &input, + r#" +contract A { + function main() public returns (word) { return 1; } +} + +contract B { + function main() public returns (word) { return 2; } +} +"#, + ) + .expect("write source"); + + let multi = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--emit-yul") + .arg(&input) + .output() + .expect("run driver yul"); + assert!( + !multi.status.success(), + "driver unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&multi.stdout), + String::from_utf8_lossy(&multi.stderr) + ); + let stderr = strip_ansi(&String::from_utf8_lossy(&multi.stderr)); + assert!( + stderr.contains("strict-assembly output requires one top-level object"), + "stderr:\n{stderr}" + ); + assert!(stderr.contains("ADeploy"), "stderr:\n{stderr}"); + assert!(stderr.contains("BDeploy"), "stderr:\n{stderr}"); + + let selected = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg("--emit-yul") + .arg("--emit-yul-object=ADeploy") + .arg(&input) + .output() + .expect("run driver selected yul"); + assert!( + selected.status.success(), + "driver failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&selected.stdout), + String::from_utf8_lossy(&selected.stderr) + ); + let yul = String::from_utf8_lossy(&selected.stdout); + assert!(yul.contains("object \"ADeploy\""), "{yul}"); + assert!(!yul.contains("object \"BDeploy\""), "{yul}"); + + let _ = fs::remove_dir_all(&dir); +} + +fn driver_stderr(label: &str, source: &str) -> String { + let dir = temp_dir(label); + fs::create_dir_all(&dir).expect("create temp dir"); + let input = dir.join("main.solc"); + fs::write(&input, source).expect("write source"); + + let output = Command::new(env!("CARGO_BIN_EXE_solcore-driver")) + .arg(&input) + .output() + .expect("run driver"); + + let _ = fs::remove_dir_all(&dir); + + assert!(!output.status.success(), "driver unexpectedly succeeded"); + strip_ansi(&String::from_utf8_lossy(&output.stderr)) +} + +fn write_fake_std(std_root: &Path) { + fs::create_dir_all(std_root).expect("create fake std root"); + fs::write( + std_root.join("std.solc"), + "export { solcoreTempStdValue };\nfunction solcoreTempStdValue() returns (word) { return 7; }\n", + ) + .expect("write fake std"); +} + +fn write_fake_std_importer(path: &Path) { + fs::write( + path, + "import * as std from std;\nfunction main() returns (word) { return std.solcoreTempStdValue(); }\n", + ) + .expect("write fake std importer"); +} + +fn temp_dir(label: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "solcore-driver-typeck-{label}-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time after epoch") + .as_nanos() + )) +} + +fn strip_ansi(input: &str) -> String { + let mut output = String::new(); + let mut chars = input.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\u{1b}' && chars.peek() == Some(&'[') { + chars.next(); + for code in chars.by_ref() { + if ('@'..='~').contains(&code) { + break; + } + } + } else { + output.push(ch); + } + } + output +} diff --git a/crates/hir-ty/Cargo.toml b/crates/hir-ty/Cargo.toml new file mode 100644 index 00000000..a9686a8b --- /dev/null +++ b/crates/hir-ty/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "solcore-hir-ty" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +ena = { workspace = true } +hir = { workspace = true } +matchcov = { workspace = true } +nameres = { workspace = true } +num-bigint = { workspace = true } +parser = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +tablesolve = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +dir-test = "0.4.1" +proptest = { workspace = true } +solcore-test-utils = { path = "../test-utils" } +url = { workspace = true } diff --git a/crates/hir-ty/src/alias.rs b/crates/hir-ty/src/alias.rs new file mode 100644 index 00000000..f27f0bca --- /dev/null +++ b/crates/hir-ty/src/alias.rs @@ -0,0 +1,634 @@ +//! Shared type-alias normalization for inference and solver lowering. + +use hir::{ + Db as HirDb, + anchor::DefId, + ast::item::{ContractItem, Item, Module, TypeAlias}, + diag::LabelSpan, + nameres::{self as hir_nameres, type_var_bindings}, + span::Spanned, +}; +use nameres::ModuleId; +use rustc_hash::FxHashSet; + +use crate::{ + BinderEnv, Db, Pred, PredKind, QualTy, Ty, TyCtor, TyKind, TyScheme, TypeLowering, + UserTyCtorKind, support::module_for_def_via_tree as module_for_def, +}; + +/// Maximum number of type nodes visited while normalizing one alias-rooted +/// type. +const DEFAULT_ALIAS_NORMALIZATION_NODE_BUDGET: usize = 16_384; + +/// Alias-normalization diagnostic independent of the final typecheck surface. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum AliasError { + /// A recursive type alias was encountered. + Cycle { + /// Source span for the alias declaration or use. + span: LabelSpan, + /// Alias name. + alias: String, + }, + /// A type alias was applied with the wrong number of arguments. + Arity { + /// Source span for the alias declaration or use. + span: LabelSpan, + /// Alias name. + alias: String, + /// Declared arity. + expected: usize, + /// Actual argument count. + actual: usize, + }, + /// Type-alias expansion exceeded the normalizer's node budget. + ExpansionLimit { + /// Source span for the alias declaration or use. + span: LabelSpan, + /// Maximum number of type nodes visited while expanding aliases. + limit: usize, + }, +} + +/// Generic view of a type shape that can contain aliases. +pub enum AliasTypeKind<'db, T> { + /// Error sentinel. + Error, + /// Unknown placeholder. + Unknown, + /// Bound variable. + BoundVar(u32), + /// Type constructor application. + Named { ctor: TyCtor<'db>, args: Vec }, + /// Function type. + Function { params: Vec, ret: T }, + /// Tuple type. + Tuple(Vec), + /// Comptime wrapper. + Comptime(T), +} + +/// Type representation supported by the shared alias normalizer. +pub trait AliasType<'db>: Clone { + /// Decomposes this type into an alias-normalization view. + fn alias_kind(&self, db: &'db dyn Db) -> AliasTypeKind<'db, Self>; + + /// Constructs an error sentinel. + fn alias_error(db: &'db dyn Db) -> Self; + + /// Constructs a bound variable. + fn alias_bound(db: &'db dyn Db, index: u32) -> Self; + + /// Constructs a named type. + fn alias_named(db: &'db dyn Db, ctor: TyCtor<'db>, args: Vec) -> Self; + + /// Constructs a function type. + fn alias_function(db: &'db dyn Db, params: Vec, ret: Self) -> Self; + + /// Constructs a tuple type. + fn alias_tuple(db: &'db dyn Db, elems: Vec) -> Self; + + /// Constructs a comptime wrapper. + fn alias_comptime(db: &'db dyn Db, inner: Self) -> Self; + + /// Converts a lowered alias body into this representation, substituting + /// alias parameters with the actual arguments supplied at the use site. + fn from_alias_body(db: &'db dyn Db, ty: Ty<'db>, args: &[Self]) -> Self { + match ty.kind(db) { + TyKind::Error => Self::alias_error(db), + TyKind::Unknown => Self::alias_error(db), + TyKind::BoundVar(var) => args + .get(var.index as usize) + .cloned() + .unwrap_or_else(|| Self::alias_bound(db, var.index)), + TyKind::Named { ctor, args: inner } => Self::alias_named( + db, + *ctor, + inner + .iter() + .map(|arg| Self::from_alias_body(db, *arg, args)) + .collect(), + ), + TyKind::Function { params, ret } => Self::alias_function( + db, + params + .iter() + .map(|param| Self::from_alias_body(db, *param, args)) + .collect(), + Self::from_alias_body(db, *ret, args), + ), + TyKind::Tuple(elems) => Self::alias_tuple( + db, + elems + .iter() + .map(|elem| Self::from_alias_body(db, *elem, args)) + .collect(), + ), + TyKind::Comptime(inner) => { + Self::alias_comptime(db, Self::from_alias_body(db, *inner, args)) + } + } + } +} + +impl<'db> AliasType<'db> for Ty<'db> { + fn alias_kind(&self, db: &'db dyn Db) -> AliasTypeKind<'db, Self> { + match self.kind(db) { + TyKind::Error => AliasTypeKind::Error, + TyKind::Unknown => AliasTypeKind::Unknown, + TyKind::BoundVar(var) => AliasTypeKind::BoundVar(var.index), + TyKind::Named { ctor, args } => AliasTypeKind::Named { + ctor: *ctor, + args: args.clone(), + }, + TyKind::Function { params, ret } => AliasTypeKind::Function { + params: params.clone(), + ret: *ret, + }, + TyKind::Tuple(elems) => AliasTypeKind::Tuple(elems.clone()), + TyKind::Comptime(inner) => AliasTypeKind::Comptime(*inner), + } + } + + fn alias_error(db: &'db dyn Db) -> Self { + Ty::error(db) + } + + fn alias_bound(db: &'db dyn Db, index: u32) -> Self { + Ty::bound(db, index) + } + + fn alias_named(db: &'db dyn Db, ctor: TyCtor<'db>, args: Vec) -> Self { + Ty::named(db, ctor, args) + } + + fn alias_function(db: &'db dyn Db, params: Vec, ret: Self) -> Self { + Ty::function(db, params, ret) + } + + fn alias_tuple(db: &'db dyn Db, elems: Vec) -> Self { + Ty::tuple(db, elems) + } + + fn alias_comptime(db: &'db dyn Db, inner: Self) -> Self { + Ty::comptime(db, inner) + } +} + +/// Result of normalizing one value. +#[derive(Debug, Clone)] +pub struct AliasNorm { + /// Normalized value. + pub value: T, + /// Errors observed while normalizing. + pub errors: Vec, +} + +/// Stateful alias normalizer for one module/resolution map. +pub struct AliasNormalizer<'a, 'db> { + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &'a hir_nameres::ItemResolutionFacts<'db>, + expanding: Vec>, + errors: Vec, + remaining_nodes: usize, + budget_exhausted: bool, +} + +impl<'a, 'db> AliasNormalizer<'a, 'db> { + /// Creates a normalizer rooted at `module`. + pub fn new( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &'a hir_nameres::ItemResolutionFacts<'db>, + ) -> Self { + Self { + db, + module, + item_resolutions, + expanding: Vec::new(), + errors: Vec::new(), + remaining_nodes: DEFAULT_ALIAS_NORMALIZATION_NODE_BUDGET, + budget_exhausted: false, + } + } + + /// Normalizes aliases inside a type. + pub fn normalize_ty(&mut self, ty: T) -> T + where + T: AliasType<'db>, + { + if !self.consume_node() { + return T::alias_error(self.db); + } + match ty.alias_kind(self.db) { + AliasTypeKind::Error | AliasTypeKind::Unknown | AliasTypeKind::BoundVar(_) => ty, + AliasTypeKind::Named { ctor, args } => { + let args = args + .into_iter() + .map(|arg| self.normalize_ty(arg)) + .collect::>(); + let TyCtor::User(user) = ctor else { + return T::alias_named(self.db, ctor, args); + }; + if !matches!(user.kind, UserTyCtorKind::Alias) { + return T::alias_named(self.db, ctor, args); + } + self.expand_alias_ctor::(user.def, ctor, args) + } + AliasTypeKind::Function { params, ret } => T::alias_function( + self.db, + params + .into_iter() + .map(|param| self.normalize_ty(param)) + .collect(), + self.normalize_ty(ret), + ), + AliasTypeKind::Tuple(elems) => T::alias_tuple( + self.db, + elems + .into_iter() + .map(|elem| self.normalize_ty(elem)) + .collect(), + ), + AliasTypeKind::Comptime(inner) => T::alias_comptime(self.db, self.normalize_ty(inner)), + } + } + + /// Normalizes aliases inside a predicate. + pub fn normalize_pred(&mut self, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(self.db) { + PredKind::InClass { class, main, args } => Pred::in_class( + self.db, + *class, + self.normalize_ty(*main), + args.iter().map(|arg| self.normalize_ty(*arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => { + Pred::eq(self.db, self.normalize_ty(*lhs), self.normalize_ty(*rhs)) + } + PredKind::Error => pred, + } + } + + /// Normalizes aliases inside a qualified type. + pub fn normalize_qual_ty(&mut self, qual: QualTy<'db>) -> QualTy<'db> { + QualTy::new( + self.db, + qual.preds(self.db) + .iter() + .map(|pred| self.normalize_pred(*pred)) + .collect::>(), + self.normalize_ty(qual.ty(self.db)), + ) + } + + /// Normalizes aliases inside a scheme while preserving binders. + pub fn normalize_scheme(&mut self, scheme: TyScheme<'db>) -> TyScheme<'db> { + TyScheme::new( + self.db, + scheme.binder_count(self.db), + self.normalize_qual_ty(scheme.body(self.db)), + ) + } + + /// Takes accumulated errors. + pub fn take_errors(&mut self) -> Vec { + std::mem::take(&mut self.errors) + } + + fn expand_alias_ctor(&mut self, def: DefId<'db>, ctor: TyCtor<'db>, args: Vec) -> T + where + T: AliasType<'db>, + { + if self.expanding.contains(&def) { + self.errors.push(AliasError::Cycle { + span: alias_label_span(self.db, self.module, def), + alias: alias_name(self.db, def), + }); + return T::alias_error(self.db); + } + + let Some(info) = lower_type_alias_info(self.db, self.module, self.item_resolutions, def) + else { + return T::alias_named(self.db, ctor, args); + }; + + let expected = info.explicit_type_var_count; + if expected != args.len() { + self.errors.push(AliasError::Arity { + span: alias_label_span(self.db, self.module, def), + alias: alias_name(self.db, def), + expected, + actual: args.len(), + }); + return T::alias_error(self.db); + } + + self.expanding.push(def); + let captured_args = (0..info.inherited_type_var_count) + .map(|index| T::alias_bound(self.db, index as u32)) + .chain(args) + .collect::>(); + let body = T::from_alias_body(self.db, info.ty, &captured_args); + let expanded = self.normalize_ty(body); + self.expanding.pop(); + expanded + } + + fn consume_node(&mut self) -> bool { + if self.remaining_nodes == 0 { + self.report_expansion_limit(); + false + } else { + self.remaining_nodes -= 1; + true + } + } + + fn report_expansion_limit(&mut self) { + if self.budget_exhausted { + return; + } + self.budget_exhausted = true; + self.errors.push(AliasError::ExpansionLimit { + span: self.expansion_limit_span(), + limit: DEFAULT_ALIAS_NORMALIZATION_NODE_BUDGET, + }); + } + + fn expansion_limit_span(&self) -> LabelSpan { + self.expanding + .first() + .copied() + .map(|def| alias_label_span(self.db, self.module, def)) + .unwrap_or_else(|| LabelSpan::from_span(self.db, self.module.span(self.db))) + } +} + +/// Normalizes aliases inside a ground type. +pub fn normalize_ty_aliases<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + ty: Ty<'db>, +) -> AliasNorm> { + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let value = normalizer.normalize_ty(ty); + AliasNorm { + value, + errors: normalizer.take_errors(), + } +} + +/// Normalizes aliases inside a predicate. +pub fn normalize_pred_aliases<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + pred: Pred<'db>, +) -> AliasNorm> { + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let value = normalizer.normalize_pred(pred); + AliasNorm { + value, + errors: normalizer.take_errors(), + } +} + +/// Normalizes aliases inside a scheme. +pub fn normalize_scheme_aliases<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + scheme: TyScheme<'db>, +) -> AliasNorm> { + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let value = normalizer.normalize_scheme(scheme); + AliasNorm { + value, + errors: normalizer.take_errors(), + } +} + +/// Checks all type-alias declarations in a module for recursive definitions +/// and malformed alias applications. +pub fn type_alias_normalization_errors<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, +) -> Vec { + let mut errors = Vec::new(); + for info in type_alias_infos(db, module, &[]) { + let ty = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_type_alias(info.alias) + .ty; + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + normalizer.expanding.push(info.alias.def_id_value(db)); + normalizer.normalize_ty::>(ty); + let alias_errors = normalizer.take_errors(); + let hit_expansion_limit = alias_errors + .iter() + .any(|error| matches!(error, AliasError::ExpansionLimit { .. })); + errors.extend(alias_errors); + if hit_expansion_limit { + break; + } + } + dedup_errors(errors) +} + +struct LoweredAliasInfo<'db> { + ty: Ty<'db>, + inherited_type_var_count: usize, + explicit_type_var_count: usize, +} + +struct TypeAliasInfo<'db> { + alias: TypeAlias<'db>, + type_vars: Vec>, + inherited_type_var_count: usize, +} + +fn alias_label_span<'db>(db: &'db dyn Db, module: Module<'db>, def: DefId<'db>) -> LabelSpan { + let span = find_type_alias_info(db, module, def, &[]) + .or_else(|| { + module_for_def(db, def) + .and_then(|module| scope_resolution_for_module_id(db, module)) + .and_then(|(scope, _)| find_type_alias_info(db, scope.module, def, &[])) + }) + .map(|info| info.alias.name_elem(db).span(db)) + .unwrap_or_else(|| module.span(db)); + LabelSpan::from_span(db, span) +} + +fn lower_type_alias_info<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + def: DefId<'db>, +) -> Option> { + if let Some(info) = find_type_alias_info(db, module, def, &[]) { + let ty = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_type_alias(info.alias) + .ty; + return Some(LoweredAliasInfo { + ty, + inherited_type_var_count: info.inherited_type_var_count, + explicit_type_var_count: info.type_vars.len() - info.inherited_type_var_count, + }); + } + + let module = module_for_def(db, def)?; + let (scope, item_resolutions) = scope_resolution_for_module_id(db, module)?; + let info = find_type_alias_info(db, scope.module, def, &[])?; + let ty = TypeLowering::from_item_resolutions( + db, + &item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_type_alias(info.alias) + .ty; + Some(LoweredAliasInfo { + ty, + inherited_type_var_count: info.inherited_type_var_count, + explicit_type_var_count: info.type_vars.len() - info.inherited_type_var_count, + }) +} + +fn type_alias_infos<'db>( + db: &'db dyn Db, + module: Module<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Vec> { + let mut result = Vec::new(); + for item in module.items(db) { + collect_type_alias_infos(db, *item, inherited, &mut result); + } + result +} + +fn collect_type_alias_infos<'db>( + db: &'db dyn Db, + item: Item<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + result: &mut Vec>, +) { + match item { + Item::TypeAlias(alias) => { + let inherited_type_var_count = inherited.len(); + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + alias.def_id_value(db), + alias.ty_param_elems(db), + )); + result.push(TypeAliasInfo { + alias, + type_vars, + inherited_type_var_count, + }); + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + for item in contract.items(db) { + if let ContractItem::TypeAlias(alias) = *item { + collect_type_alias_infos(db, Item::TypeAlias(alias), &inherited, result); + } + } + } + _ => {} + } +} + +fn find_type_alias_info<'db>( + db: &'db dyn Db, + module: Module<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_type_alias_in_item(db, *item, def, inherited)) +} + +fn find_type_alias_in_item<'db>( + db: &'db dyn Db, + item: Item<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + match item { + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + let inherited_type_var_count = inherited.len(); + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + alias.def_id_value(db), + alias.ty_param_elems(db), + )); + Some(TypeAliasInfo { + alias, + type_vars, + inherited_type_var_count, + }) + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + contract.items(db).iter().find_map(|item| match *item { + ContractItem::TypeAlias(alias) => { + find_type_alias_in_item(db, Item::TypeAlias(alias), def, &inherited) + } + ContractItem::FunctionDef(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }) + } + _ => None, + } +} + +fn alias_name<'db>(db: &'db dyn HirDb, def: DefId<'db>) -> String { + def.name(db) + .unwrap_or_else(|| format!("{:?}", def.kind(db))) +} + +fn scope_resolution_for_module_id<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Option<( + hir_nameres::ItemScopeFacts<'db>, + hir_nameres::ItemResolutionFacts<'db>, +)> { + let env = nameres::module_import_surface(db, module); + let scope = env.item_scope.clone()?; + let item_resolutions = + hir_nameres::resolve_item_type_facts_with_imports(db, scope.module, &scope, &env); + Some((scope, item_resolutions)) +} + +fn dedup_errors(errors: Vec) -> Vec { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for error in errors { + if seen.insert(error.clone()) { + result.push(error); + } + } + result +} diff --git a/crates/hir-ty/src/contract/abi.rs b/crates/hir-ty/src/contract/abi.rs new file mode 100644 index 00000000..c5e75234 --- /dev/null +++ b/crates/hir-ty/src/contract/abi.rs @@ -0,0 +1,757 @@ +use hir::{ + anchor::DefId, + ast::item::{AdtDef, ContractItem, Item, Module}, + diag::Diagnostic, +}; +use nameres::{LibraryId, module_id_for_source_file}; +use parser::parse_file_to_hir; + +use crate::{ + BuiltinTyCtor, Db, Ty, TyCtor, TyKind, UserTyCtor, UserTyCtorKind, value_type_underlying, +}; + +/// ABI parameter or tuple component. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct AbiParam { + /// Parameter name. Unnamed outputs and tuple components use the empty name, + /// matching the reference ABI emitter. + pub name: String, + /// Canonical ABI type. + pub ty: AbiType, + /// Tuple components, if `ty` is `AbiType::Tuple`. + pub components: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum AbiType { + Uint256, + Bool, + String, + Unit, + Tuple, + Named(String), + Unsupported, +} + +impl std::fmt::Display for AbiType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AbiType::Uint256 => f.write_str("uint256"), + AbiType::Bool => f.write_str("bool"), + AbiType::String => f.write_str("string"), + AbiType::Unit => Ok(()), + AbiType::Tuple => f.write_str("tuple"), + AbiType::Named(name) => f.write_str(name), + AbiType::Unsupported => f.write_str(""), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct AbiSelector(pub [u8; 4]); + +impl AbiSelector { + pub fn to_hex(self) -> String { + format!( + "0x{:02x}{:02x}{:02x}{:02x}", + self.0[0], self.0[1], self.0[2], self.0[3] + ) + } +} + +/// Interned ABI signature preimage used as the selector query key. +#[salsa::interned(debug)] +pub struct AbiSignature<'db> { + /// Canonical signature, e.g. `transfer(address,uint256)`. + #[returns(ref)] + pub text: String, +} + +/// Computes the ABI selector for a canonical signature. +#[salsa::tracked] +pub fn abi_selector<'db>(db: &'db dyn Db, signature: AbiSignature<'db>) -> AbiSelector { + let hash = hir::keccak::keccak256(signature.text(db).as_bytes()); + AbiSelector([hash[0], hash[1], hash[2], hash[3]]) +} + +pub(super) fn method_signature_string<'db>( + db: &'db dyn Db, + name: &str, + params: &[Ty<'db>], +) -> Result { + let mut out = String::new(); + out.push_str(name); + out.push('('); + for (index, param) in params.iter().enumerate() { + if index > 0 { + out.push(','); + } + out.push_str(&signature_type_string(db, *param, &mut Vec::new())?); + } + out.push(')'); + Ok(out) +} + +fn signature_type_string<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + adt_stack: &mut Vec>, +) -> Result { + if ty_contains_fixed_array(db, ty) { + return Err(unsupported_fixed_array_abi_type(db, ty)); + } + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } if args.is_empty() => Ok(AbiType::Uint256.to_string()), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Ok(AbiType::Bool.to_string()), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::String), + args, + } if args.is_empty() => Ok(AbiType::String.to_string()), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Ok(AbiType::Unit.to_string()), + TyKind::Tuple(elems) => tuple_signature_string(db, elems, adt_stack), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => tuple_signature_string(db, args, adt_stack), + TyKind::Named { + ctor: TyCtor::User(user), + args, + } => { + if user.kind == UserTyCtorKind::ValueType { + return Err(unsupported_value_type_abi_type(db, user, args)); + } + if args.is_empty() + && let Some(name) = canonical_user_abi_name(db, user) + { + return Ok(name); + } + if let Some(name) = canonical_location_abi_name(db, user, args)? { + return Ok(name); + } + reject_structural_std_abi_fallback(db, user, args)?; + Err(unsupported_user_adt_abi_type(db, user, args)) + } + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => Err(ty.display(db)), + TyKind::Named { .. } | TyKind::Function { .. } | TyKind::Comptime(_) => Err(ty.display(db)), + } +} + +fn tuple_signature_string<'db>( + db: &'db dyn Db, + elems: &[Ty<'db>], + adt_stack: &mut Vec>, +) -> Result { + let mut parts = Vec::new(); + for elem in flatten_tuple(db, elems) { + parts.push(signature_type_string(db, elem, adt_stack)?); + } + Ok(format!("({})", parts.join(","))) +} + +pub(super) fn abi_params<'db>( + db: &'db dyn Db, + names: &[String], + tys: &[Ty<'db>], + diagnostics: &mut Vec, + span: hir::span::Span<'db>, +) -> Vec { + tys.iter() + .enumerate() + .map(|(index, ty)| { + match abi_param( + db, + names.get(index).cloned().unwrap_or_default(), + *ty, + &mut Vec::new(), + ) { + Ok(param) => param, + Err(err) => { + diagnostics.push(contract_diag_unsupported_abi_type( + db, + span, + "ABI parameter", + &err, + )); + AbiParam { + name: names.get(index).cloned().unwrap_or_default(), + ty: AbiType::Unsupported, + components: Vec::new(), + } + } + } + }) + .collect() +} + +pub(super) fn abi_outputs<'db>( + db: &'db dyn Db, + names: &[String], + ty: Ty<'db>, + diagnostics: &mut Vec, + span: hir::span::Span<'db>, +) -> Vec { + if is_unit_ty(db, ty) { + return Vec::new(); + } + flatten_output_ty(db, ty) + .into_iter() + .enumerate() + .map(|(index, ty)| { + match abi_param( + db, + names.get(index).cloned().unwrap_or_default(), + ty, + &mut Vec::new(), + ) { + Ok(param) => param, + Err(err) => { + diagnostics.push(contract_diag_unsupported_abi_type( + db, + span, + "ABI output", + &err, + )); + AbiParam { + name: names.get(index).cloned().unwrap_or_default(), + ty: AbiType::Unsupported, + components: Vec::new(), + } + } + } + }) + .collect() +} + +fn abi_param<'db>( + db: &'db dyn Db, + name: String, + ty: Ty<'db>, + adt_stack: &mut Vec>, +) -> Result { + let (ty, components) = abi_type_of(db, ty, adt_stack)?; + Ok(AbiParam { + name, + ty, + components, + }) +} + +fn abi_type_of<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + adt_stack: &mut Vec>, +) -> Result<(AbiType, Vec), String> { + if ty_contains_fixed_array(db, ty) { + return Err(unsupported_fixed_array_abi_type(db, ty)); + } + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } if args.is_empty() => Ok((AbiType::Uint256, Vec::new())), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Ok((AbiType::Bool, Vec::new())), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::String), + args, + } if args.is_empty() => Ok((AbiType::String, Vec::new())), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Ok((AbiType::Unit, Vec::new())), + TyKind::Tuple(elems) if elems.is_empty() => Ok((AbiType::Unit, Vec::new())), + TyKind::Tuple(elems) => Ok(( + AbiType::Tuple, + flatten_tuple(db, elems) + .into_iter() + .map(|elem| abi_param(db, String::new(), elem, adt_stack)) + .collect::, _>>()?, + )), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => Ok(( + AbiType::Tuple, + flatten_tuple(db, args) + .into_iter() + .map(|elem| abi_param(db, String::new(), elem, adt_stack)) + .collect::, _>>()?, + )), + TyKind::Named { + ctor: TyCtor::User(user), + args, + } => { + if user.kind == UserTyCtorKind::ValueType { + return Err(unsupported_value_type_abi_type(db, user, args)); + } + if args.is_empty() + && let Some(name) = canonical_user_abi_name(db, user) + { + return Ok((AbiType::Named(name), Vec::new())); + } + if let Some(name) = canonical_location_abi_name(db, user, args)? { + return Ok((AbiType::Named(name), Vec::new())); + } + reject_structural_std_abi_fallback(db, user, args)?; + Err(unsupported_user_adt_abi_type(db, user, args)) + } + _ => Err(ty.display(db)), + } +} + +fn canonical_user_abi_name(db: &dyn Db, user: &UserTyCtor<'_>) -> Option { + let name = user.def.name(db)?; + if !is_canonical_std_def_named(db, user.def, &name) { + return None; + } + match name.as_str() { + "uint256" | "address" | "bytes32" => Some(name), + _ => None, + } +} + +fn canonical_location_abi_name<'db>( + db: &'db dyn Db, + user: &UserTyCtor<'db>, + args: &[Ty<'db>], +) -> Result, String> { + let Some(name) = user.def.name(db) else { + return Ok(None); + }; + if !matches!(name.as_str(), "memory" | "calldata" | "storage" | "mapping") + || !is_canonical_std_def_named(db, user.def, &name) + { + return Ok(None); + } + if name != "memory" { + return Err(format!( + "{} ({name} values are not supported by the canonical external ABI)", + Ty::named(db, TyCtor::User(*user), args.to_vec()).display(db) + )); + } + let [inner] = args else { + return Err(format!( + "memory (expected one type argument, found {})", + args.len() + )); + }; + let TyKind::Named { + ctor: TyCtor::User(inner_user), + args: inner_args, + } = inner.kind(db) + else { + return Err(format!( + "{} (only memory(string) and memory(bytes) have canonical ABI evidence)", + inner.display(db) + )); + }; + if !inner_args.is_empty() { + return Err(format!( + "{} (only memory(string) and memory(bytes) have canonical ABI evidence)", + inner.display(db) + )); + } + let Some(inner_name) = inner_user.def.name(db) else { + return Ok(None); + }; + if matches!(inner_name.as_str(), "string" | "bytes") + && is_canonical_std_def_named(db, inner_user.def, &inner_name) + { + return Ok(Some(inner_name)); + } + Err(format!( + "{} (only memory(string) and memory(bytes) have canonical ABI evidence)", + inner.display(db) + )) +} + +fn is_canonical_std_def_named(db: &dyn Db, def: DefId<'_>, name: &str) -> bool { + def.name(db).as_deref() == Some(name) + && module_id_for_source_file(db, def.file(db)).is_some_and(|module| { + module.library(db) == &LibraryId::Std && module.logical_path(db).as_slice() == ["std"] + }) +} + +fn reject_structural_std_abi_fallback<'db>( + db: &'db dyn Db, + user: &UserTyCtor<'db>, + args: &[Ty<'db>], +) -> Result<(), String> { + let is_std = module_id_for_source_file(db, user.def.file(db)) + .is_some_and(|module| module.library(db) == &LibraryId::Std); + if !is_std { + return Ok(()); + } + let name = user + .def + .name(db) + .unwrap_or_else(|| "".to_owned()); + Err(format!( + "{} (standard-library type `{name}` has no canonical external ABI evidence)", + Ty::named(db, TyCtor::User(*user), args.to_vec()).display(db) + )) +} + +fn unsupported_user_adt_abi_type<'db>( + db: &'db dyn Db, + user: &UserTyCtor<'db>, + args: &[Ty<'db>], +) -> String { + let ty = Ty::named(db, TyCtor::User(*user), args.to_vec()); + format!( + "{} (user-defined ADTs are not supported by the canonical external ABI)", + crate::display::display_ty_source(db, ty, &[]) + ) +} + +fn unsupported_value_type_abi_type<'db>( + db: &'db dyn Db, + user: &UserTyCtor<'db>, + args: &[Ty<'db>], +) -> String { + let ty = Ty::named(db, TyCtor::User(*user), args.to_vec()); + format!( + "{} (user-defined value types are not supported by the canonical external ABI)", + crate::display::display_ty_source(db, ty, &[]) + ) +} + +fn unsupported_fixed_array_abi_type(db: &dyn Db, ty: Ty<'_>) -> String { + format!( + "{} (fixed-length arrays are not yet supported by the canonical external ABI)", + crate::display::display_ty_source(db, ty, &[]) + ) +} + +fn ty_contains_fixed_array(db: &dyn Db, ty: Ty<'_>) -> bool { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => true, + TyKind::Named { args, .. } | TyKind::Tuple(args) => { + args.iter().any(|arg| ty_contains_fixed_array(db, *arg)) + } + TyKind::Function { params, ret } => { + params + .iter() + .any(|param| ty_contains_fixed_array(db, *param)) + || ty_contains_fixed_array(db, *ret) + } + TyKind::Comptime(inner) => ty_contains_fixed_array(db, *inner), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + } +} + +fn user_adt_product_fields<'db>( + db: &'db dyn Db, + user: &UserTyCtor<'db>, + args: &[Ty<'db>], + adt_stack: &mut Vec>, +) -> Result>, String> { + if adt_stack.contains(&user.def) { + return Err(format!( + "{} (recursive ADTs have no finite canonical ABI tuple)", + user.def + .name(db) + .unwrap_or_else(|| "".to_owned()) + )); + } + let module = parse_file_to_hir(db, user.def.file(db)).module(db); + let name = user + .def + .name(db) + .unwrap_or_else(|| "".to_owned()); + if generic_derivation_is_excluded(db, module, &name) { + return Err(format!( + "{name} (manual or excluded Generic representations are not canonical ABI layouts)" + )); + } + let adt = find_adt_by_def(db, module, user.def).ok_or_else(|| { + format!( + "{} (definition is unavailable for ABI lowering)", + user.def + .name(db) + .unwrap_or_else(|| "".to_owned()) + ) + })?; + let ctor = match adt.ctors(db).as_slice() { + [ctor] => ctor, + [] => { + return Err(format!( + "{name} (constructorless ADTs have no canonical ABI representation)" + )); + } + [_, _, ..] => { + return Err(format!( + "{name} (multi-constructor ADTs have no canonical ABI representation)" + )); + } + }; + if ctor.field_count == 0 { + return Err(format!( + "{name} (zero-field ADTs have no canonical ABI tuple representation)" + )); + } + if adt.ty_param_elems(db).len() != args.len() { + return Err(format!( + "{name} (expected {} ABI type arguments, found {})", + adt.ty_param_elems(db).len(), + args.len() + )); + } + let plan = crate::solver::derived_generic_plan(db, module, adt) + .ok_or_else(|| format!("{name} (cannot derive its Generic ABI representation)"))?; + let product_rep = plan + .from_arms + .first() + .map(|arm| substitute_bound_tys(db, arm.product_rep, args)) + .ok_or_else(|| format!("{name} (cannot derive its constructor product representation)"))?; + let fields = split_constructor_product(db, product_rep, ctor.field_count).ok_or_else(|| { + format!( + "{name} (its Generic product does not match the source constructor arity {})", + ctor.field_count + ) + })?; + if fields.iter().any(|field| is_abi_tuple_shape(db, *field)) { + return Err(format!( + "{name} (tuple-typed constructor fields are unsupported because Generic erases their ABI tuple boundary)" + )); + } + adt_stack.push(user.def); + Ok(fields) +} + +fn generic_derivation_is_excluded(db: &dyn Db, module: Module<'_>, adt_name: &str) -> bool { + module.items(db).iter().any(|item| { + let Item::Pragma(pragma) = item else { + return false; + }; + (*pragma.name(db).atom()).text(db) == "noGenericInstanceFor" + && pragma + .items(db) + .iter() + .any(|item| (*item.atom()).text(db) == adt_name) + }) +} + +fn find_adt_by_def<'db>( + db: &'db dyn Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => Some(*adt), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match item { + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => Some(*adt), + _ => None, + }), + _ => None, + }) +} + +fn substitute_bound_tys<'db>(db: &'db dyn Db, ty: Ty<'db>, args: &[Ty<'db>]) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => args.get(var.index as usize).copied().unwrap_or(ty), + TyKind::Named { ctor, args: inner } => Ty::named( + db, + *ctor, + inner + .iter() + .map(|ty| substitute_bound_tys(db, *ty, args)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|ty| substitute_bound_tys(db, *ty, args)) + .collect(), + substitute_bound_tys(db, *ret, args), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|ty| substitute_bound_tys(db, *ty, args)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, substitute_bound_tys(db, *inner, args)), + TyKind::Error | TyKind::Unknown => ty, + } +} + +fn split_constructor_product<'db>( + db: &'db dyn Db, + mut product: Ty<'db>, + arity: usize, +) -> Option>> { + if arity == 0 { + return None; + } + let mut fields = Vec::with_capacity(arity); + for _ in 1..arity { + let TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } = product.kind(db) + else { + return None; + }; + if args.len() != 2 { + return None; + } + fields.push(args[0]); + product = args[1]; + } + fields.push(product); + Some(fields) +} + +fn is_abi_tuple_shape(db: &dyn Db, ty: Ty<'_>) -> bool { + match ty.kind(db) { + TyKind::Tuple(_) => true, + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } => args.len() == 2, + _ => false, + } +} + +pub(super) fn abi_type_contains_user_adt<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + target: DefId<'db>, +) -> bool { + fn visit<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + target: DefId<'db>, + adt_stack: &mut Vec>, + ) -> bool { + match ty.kind(db) { + TyKind::Tuple(elems) => elems.iter().any(|elem| visit(db, *elem, target, adt_stack)), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => args.iter().any(|elem| visit(db, *elem, target, adt_stack)), + TyKind::Named { + ctor: TyCtor::User(user), + args, + } if user.kind == UserTyCtorKind::ValueType && args.is_empty() => { + value_type_underlying(db, user.def) + .is_ok_and(|underlying| visit(db, underlying, target, adt_stack)) + } + TyKind::Named { + ctor: TyCtor::User(user), + args, + } if is_canonical_std_location(db, user) && args.len() == 1 => { + visit(db, args[0], target, adt_stack) + } + TyKind::Named { + ctor: TyCtor::User(user), + args, + } if args.is_empty() && canonical_user_abi_name(db, user).is_some() => false, + TyKind::Named { + ctor: TyCtor::User(user), + args, + } => { + if user.def == target { + return true; + } + let Ok(fields) = user_adt_product_fields(db, user, args, adt_stack) else { + return false; + }; + let found = fields + .into_iter() + .any(|field| visit(db, field, target, adt_stack)); + adt_stack.pop(); + found + } + TyKind::Comptime(inner) => visit(db, *inner, target, adt_stack), + TyKind::Error + | TyKind::Unknown + | TyKind::BoundVar(_) + | TyKind::Named { .. } + | TyKind::Function { .. } => false, + } + } + + visit(db, ty, target, &mut Vec::new()) +} + +fn flatten_output_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Vec> { + match ty.kind(db) { + TyKind::Tuple(elems) => flatten_tuple(db, elems), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => flatten_tuple(db, args), + _ => vec![ty], + } +} + +fn flatten_tuple<'db>(db: &'db dyn Db, elems: &[Ty<'db>]) -> Vec> { + let mut out = Vec::new(); + for elem in elems { + match elem.kind(db) { + TyKind::Tuple(nested) => out.extend(flatten_tuple(db, nested)), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => out.extend(flatten_tuple(db, args)), + _ => out.push(*elem), + } + } + out +} + +fn is_unit_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + matches!( + ty.kind(db), + TyKind::Tuple(elems) if elems.is_empty() + ) || matches!( + ty.kind(db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() + ) +} + +pub(super) fn abi_params_contain_unsupported(params: &[AbiParam]) -> bool { + params.iter().any(|param| { + matches!(¶m.ty, AbiType::Unsupported) + || abi_params_contain_unsupported(¶m.components) + }) +} + +fn is_canonical_std_location(db: &dyn Db, user: &UserTyCtor<'_>) -> bool { + user.def.name(db).is_some_and(|name| { + matches!(name.as_str(), "memory" | "calldata" | "storage" | "mapping") + && is_canonical_std_def_named(db, user.def, &name) + }) +} + +pub(super) fn contract_diag_unsupported_abi_type<'db>( + db: &'db dyn Db, + span: hir::span::Span<'db>, + context: &str, + ty: &str, +) -> Diagnostic { + Diagnostic::error(format!("{context} cannot be represented in the ABI: {ty}")) + .with_code("SC0231") + .with_primary_label(db, span, Some("unsupported ABI type")) +} diff --git a/crates/hir-ty/src/contract/abi_json.rs b/crates/hir-ty/src/contract/abi_json.rs new file mode 100644 index 00000000..ddcf536e --- /dev/null +++ b/crates/hir-ty/src/contract/abi_json.rs @@ -0,0 +1,269 @@ +use std::fmt::Write as _; + +use hir::ast::{ + function::FunctionMutability, + item::{ContractDef, Module}, +}; + +use super::{ + abi::{AbiParam, AbiType, abi_params_contain_unsupported}, + dispatch::{DispatchConstructor, DispatchFallback, contract_dispatch_surface}, +}; +use crate::Db; + +/// Renders an ABI JSON document mirroring the reference `contractAbiJson` +/// behavior: explicit constructors and user-defined fallbacks are included, +/// while the implicit runtime defaults remain a dispatch-surface detail. +pub fn contract_abi_json<'db>( + db: &'db dyn Db, + module: Module<'db>, + contract: ContractDef<'db>, +) -> Result { + let surface = contract_dispatch_surface(db, module, contract); + if surface.methods.iter().any(|method| { + abi_params_contain_unsupported(&method.inputs) + || abi_params_contain_unsupported(&method.outputs) + }) || matches!( + &surface.constructor, + DispatchConstructor::Explicit { inputs, .. } + if abi_params_contain_unsupported(inputs) + ) || matches!( + &surface.fallback, + DispatchFallback::Explicit { + inputs, outputs, .. + } if abi_params_contain_unsupported(inputs) + || abi_params_contain_unsupported(outputs) + ) { + return Err("cannot represent unsupported type in ABI".to_owned()); + } + let mut entries = Vec::new(); + if let DispatchConstructor::Explicit { + source_index, + inputs, + payable, + } = surface.constructor + { + entries.push((source_index, AbiJsonEntry::Constructor { inputs, payable })); + } + for method in surface.methods { + entries.push(( + method.source_index, + AbiJsonEntry::Function { + name: method.name, + inputs: method.inputs, + outputs: method.outputs, + mutability: method.mutability, + }, + )); + } + if let DispatchFallback::Explicit { + source_index, + payable, + .. + } = surface.fallback + { + entries.push((source_index, AbiJsonEntry::Fallback { payable })); + } + entries.sort_by_key(|(source_index, _)| *source_index); + let entries = entries + .into_iter() + .map(|(_, entry)| entry) + .collect::>(); + render_abi_json(&entries) +} + +enum AbiJsonEntry { + Function { + name: String, + inputs: Vec, + outputs: Vec, + mutability: Option, + }, + Constructor { + inputs: Vec, + payable: bool, + }, + Fallback { + payable: bool, + }, +} + +fn render_abi_json(entries: &[AbiJsonEntry]) -> Result { + let mut out = String::new(); + if entries.is_empty() { + out.push_str("[]\n"); + return Ok(out); + } + out.push_str("[\n"); + for (index, entry) in entries.iter().enumerate() { + if index > 0 { + out.push_str(",\n"); + } + render_abi_entry(&mut out, entry, 1)?; + } + out.push_str("\n]\n"); + Ok(out) +} + +fn render_abi_entry(out: &mut String, entry: &AbiJsonEntry, ind: usize) -> Result<(), String> { + match entry { + AbiJsonEntry::Function { + name, + inputs, + outputs, + mutability, + } => { + line(out, ind, "{"); + render_named_params(out, ind + 1, "inputs", inputs, true)?; + line(out, ind + 1, &format!("\"name\": {},", json_string(name))); + render_named_params(out, ind + 1, "outputs", outputs, true)?; + line( + out, + ind + 1, + &format!( + "\"stateMutability\": \"{}\",", + function_state_mutability(*mutability) + ), + ); + line(out, ind + 1, "\"type\": \"function\""); + push_close_brace(out, ind); + } + AbiJsonEntry::Constructor { inputs, payable } => { + line(out, ind, "{"); + render_named_params(out, ind + 1, "inputs", inputs, true)?; + line( + out, + ind + 1, + &format!("\"stateMutability\": \"{}\",", payability_state(*payable)), + ); + line(out, ind + 1, "\"type\": \"constructor\""); + push_close_brace(out, ind); + } + AbiJsonEntry::Fallback { payable } => { + line(out, ind, "{"); + line( + out, + ind + 1, + &format!("\"stateMutability\": \"{}\",", payability_state(*payable)), + ); + line(out, ind + 1, "\"type\": \"fallback\""); + push_close_brace(out, ind); + } + } + Ok(()) +} + +fn render_named_params( + out: &mut String, + ind: usize, + name: &str, + params: &[AbiParam], + trailing_comma: bool, +) -> Result<(), String> { + if params.iter().any(abi_param_is_unsupported) { + return Err("cannot represent type in ABI".to_owned()); + } + if params.is_empty() { + line( + out, + ind, + &format!("\"{name}\": []{}", if trailing_comma { "," } else { "" }), + ); + return Ok(()); + } + line(out, ind, &format!("\"{name}\": [")); + for (index, param) in params.iter().enumerate() { + if index > 0 { + out.push_str(",\n"); + } + render_abi_param(out, ind + 1, param)?; + } + out.push('\n'); + line( + out, + ind, + &format!("]{}", if trailing_comma { "," } else { "" }), + ); + Ok(()) +} + +fn render_abi_param(out: &mut String, ind: usize, param: &AbiParam) -> Result<(), String> { + let ty = param.ty.to_string(); + line(out, ind, "{"); + line( + out, + ind + 1, + &format!("\"internalType\": {},", json_string(&ty)), + ); + line( + out, + ind + 1, + &format!("\"name\": {},", json_string(¶m.name)), + ); + line( + out, + ind + 1, + &format!( + "\"type\": {}{}", + json_string(&ty), + if param.components.is_empty() { "" } else { "," } + ), + ); + if !param.components.is_empty() { + render_named_params(out, ind + 1, "components", ¶m.components, false)?; + } + push_close_brace(out, ind); + Ok(()) +} + +fn abi_param_is_unsupported(param: &AbiParam) -> bool { + matches!(¶m.ty, AbiType::Unsupported) + || param.components.iter().any(abi_param_is_unsupported) +} + +fn payability_state(payable: bool) -> &'static str { + if payable { "payable" } else { "nonpayable" } +} + +fn function_state_mutability(mutability: Option) -> &'static str { + match mutability { + None => "nonpayable", + Some(FunctionMutability::Pure) => "pure", + Some(FunctionMutability::View) => "view", + Some(FunctionMutability::Payable) => "payable", + } +} + +fn line(out: &mut String, ind: usize, text: &str) { + push_indent(out, ind); + out.push_str(text); + out.push('\n'); +} + +fn push_close_brace(out: &mut String, ind: usize) { + push_indent(out, ind); + out.push('}'); +} + +fn push_indent(out: &mut String, ind: usize) { + for _ in 0..ind { + out.push_str(" "); + } +} + +fn json_string(value: &str) -> String { + let mut out = String::from("\""); + for ch in value.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c < '\u{20}' => write!(&mut out, "\\u{:04x}", c as u32).unwrap(), + c => out.push(c), + } + } + out.push('"'); + out +} diff --git a/crates/hir-ty/src/contract/desugar.rs b/crates/hir-ty/src/contract/desugar.rs new file mode 100644 index 00000000..1616301a --- /dev/null +++ b/crates/hir-ty/src/contract/desugar.rs @@ -0,0 +1,617 @@ +use hir::{ + anchor::DefId, + arena::Id, + ast::{ + function::{Expr, ExprKind, FuncBody, Pat, PatKind, Stmt, StmtKind}, + item::{ContractItem, FunctionDef, Item, Module}, + }, + nameres::{self as hir_nameres, is_direct_call_resolution}, +}; +use rustc_hash::FxHashMap; + +use super::helpers::{ + function_type_vars, ident_text, param_names, selector_name, type_var_bindings, +}; +use crate::{ + AliasNormalizer, BinderEnv, BodyTyContext, CallSiteCallee, CallSiteEvidence, Db, TypeLowering, + desugar::{ProductShape, SourceOrigin, SourceOriginKind}, + infer_body, trait_env_from_module_resolution, trait_env_with_givens, +}; + +/// Tracked backend-facing frontend rewrite plan for one module. +/// +/// This is intentionally separate from `crate::desugar::PreTypeckDesugarPlan`. +/// Pre-typecheck desugar changes the input view used by inference, while this +/// plan records rewrites that need resolved storage fields, ABI/dispatch +/// context, solved call-site evidence, or backend-compatibility metadata and +/// are consumed by specialization and later backend phases. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct FrontendDesugarPlan<'db> { + /// Per-body transform plan entries. + pub bodies: Vec>, +} + +/// Backend-facing transform plan for one function body. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyDesugarPlan<'db> { + /// Function/method definition. + pub function: DefId<'db>, + /// Human-readable function name. + pub function_name: String, + /// Backend-visible rewrites and hooks in traversal order. + pub transforms: Vec>, +} + +/// One planned backend-facing frontend rewrite. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum FrontendTransform<'db> { + /// `if` statement rewritten to a two-arm match on desugared bool. + IfStmtToMatch { + /// Body containing the statement. + body: FuncBody<'db>, + /// Statement being rewritten. + stmt: Id>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + }, + /// `if ... then ... else ...` expression rewritten through the same + /// true/false match scheme. + IfExprToMatch { + /// Body containing the expression. + body: FuncBody<'db>, + /// Expression being rewritten. + expr: Id>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + }, + /// Bool constructor or pattern rewritten to `inr(())` or `inl(())`. + BoolToUnitSum { + /// Body containing the node. + body: FuncBody<'db>, + /// Node category. + node: BoolNode<'db>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + /// Source constructor/pattern name. + source: String, + /// Replacement constructor. + replacement: String, + }, + /// Contract field read rewritten through an RVA storage access hook. + FieldRead { + /// Body containing the expression. + body: FuncBody<'db>, + /// Expression being rewritten. + expr: Id>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + /// Field identity. + field: hir_nameres::FieldId<'db>, + /// Generated selector type/value name. + selector: String, + /// Storage access hook for Hull/storage layout. + hook: String, + }, + /// Contract field write rewritten through an LVA/RVA assignment hook. + FieldWrite { + /// Body containing the statement. + body: FuncBody<'db>, + /// Assignment statement being rewritten. + stmt: Id>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + /// Field identity. + field: hir_nameres::FieldId<'db>, + /// Generated selector type/value name. + selector: String, + /// Storage access hook for Hull/storage layout. + hook: String, + }, + /// Non-direct call rewritten to `invokable.invoke(callee, + /// indirectArgs(args))`. + IndirectCall { + /// Body containing the call. + body: FuncBody<'db>, + /// Call expression being rewritten. + call_expr: Id>, + /// Expression used as the callee. + callee_expr: Id>, + /// User syntax that should receive diagnostics for generated nodes. + origin: SourceOrigin<'db>, + /// Callee identity used for evidence replay. + callee: CallSiteCallee<'db>, + /// Unit, single-argument, or right-nested pair payload shape. + args: IndirectArgShape<'db>, + /// Solved call-site evidence for the invokable obligation. + evidence: Option>, + }, +} + +/// Category of bool node in a frontend transform. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BoolNode<'db> { + /// Expression constructor. + Expr(Id>), + /// Pattern constructor. + Pat(Id>), +} + +/// Payload shape for an indirect-call argument tuple. +pub type IndirectArgShape<'db> = ProductShape>>; + +/// Returns the tracked backend-facing frontend rewrite plan for `module`. +/// +/// Keep purely syntactic core-language normalization in +/// [`crate::pre_typeck_desugar_plan`]. Type checking must use that pre-typeck +/// view; entries here are for specialization/backend replay and compatibility +/// metadata. +#[salsa::tracked] +pub fn frontend_desugar_plan<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> FrontendDesugarPlan<'db> { + let resolution = hir_nameres::resolve_module(db, module); + let mut bodies = Vec::new(); + for item in module.items(db) { + collect_desugar_plans(db, module, *item, &resolution, &[], &mut bodies); + } + FrontendDesugarPlan { bodies } +} + +fn collect_desugar_plans<'db>( + db: &'db dyn Db, + module: Module<'db>, + item: Item<'db>, + resolution: &hir_nameres::ModuleResolutionMap<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + out: &mut Vec>, +) { + match item { + Item::FunctionDef(function) => { + collect_function_desugar_plan( + db, + module, + function, + resolution, + inherited_type_vars, + out, + ); + } + Item::ContractDef(contract) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item { + collect_function_desugar_plan( + db, module, function, resolution, &inherited, out, + ); + } + } + } + Item::InstanceDef(instance) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + )); + for method in instance.methods(db) { + collect_function_desugar_plan(db, module, *method, resolution, &inherited, out); + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +fn collect_function_desugar_plan<'db>( + db: &'db dyn Db, + module: Module<'db>, + function: FunctionDef<'db>, + resolution: &hir_nameres::ModuleResolutionMap<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + out: &mut Vec>, +) { + let Some(body) = function.body(db) else { + return; + }; + let Some(body_map) = body_resolution_for(resolution, body) else { + return; + }; + let expr_resolutions = body_map + .exprs + .iter() + .map(|entry| ((entry.body, entry.expr), entry.resolution.clone())) + .collect::>(); + let pat_resolutions = body_map + .pats + .iter() + .map(|entry| ((entry.body, entry.pat), entry.resolution.clone())) + .collect::>(); + let call_site_evidence = desugar_inference_result( + db, + module, + function, + resolution, + body_map, + inherited_type_vars, + ) + .map(|result| { + result + .call_site_evidence + .into_iter() + .map(|evidence| { + ( + (evidence.body, evidence.call_expr, evidence.callee_expr), + evidence, + ) + }) + .collect::>() + }) + .unwrap_or_default(); + let mut collector = DesugarCollector { + db, + body, + expr_resolutions, + pat_resolutions, + call_site_evidence, + transforms: Vec::new(), + }; + for stmt in body.top_level_stmts(db) { + collector.stmt(*stmt); + } + if !collector.transforms.is_empty() { + out.push(BodyDesugarPlan { + function: function.def_id_value(db), + function_name: ident_text(db, &function.sig(db).name), + transforms: collector.transforms, + }); + } +} + +fn desugar_inference_result<'db>( + db: &'db dyn Db, + module: Module<'db>, + function: FunctionDef<'db>, + resolution: &hir_nameres::ModuleResolutionMap<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + if !body_map.diagnostics.is_empty() { + return None; + } + let body = function.body(db)?; + let sig = function.sig(db); + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(function_type_vars(db, &[], function.def_id_value(db), sig)); + let lowerer = TypeLowering::from_item_resolutions( + db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut normalizer = AliasNormalizer::new(db, module, &resolution.item_resolutions); + let mut lowered = lowerer.lower_function(function); + lowered.scheme = normalizer.normalize_scheme(lowered.scheme); + lowered.params = lowered + .params + .into_iter() + .map(|param| normalizer.normalize_ty(param)) + .collect(); + lowered.ret = normalizer.normalize_ty(lowered.ret); + let base_trait_env = trait_env_from_module_resolution(db, module, resolution); + let trait_env = trait_env_with_givens( + db, + base_trait_env, + lowered.scheme.body(db).preds(db).clone(), + ); + let pre_typeck_desugar = crate::pre_typeck_desugar_body_tree(db, body); + let ctx = BodyTyContext::new( + module, + body_map.clone(), + type_vars, + lowered.params, + Some(lowered.ret), + ) + .with_param_names(param_names(db, sig.params.atom())) + .with_ret_display( + sig.ret + .map(|ret| crate::display::display_type_ref_source(db, ret)), + ) + .with_trait_env(trait_env) + .with_pre_typeck_desugar(pre_typeck_desugar); + Some(infer_body(db, body, ctx)) +} + +struct DesugarCollector<'db> { + db: &'db dyn Db, + body: FuncBody<'db>, + expr_resolutions: FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + pat_resolutions: FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + call_site_evidence: + FxHashMap<(FuncBody<'db>, Id>, Id>), CallSiteEvidence<'db>>, + transforms: Vec>, +} + +impl<'db> DesugarCollector<'db> { + fn stmt(&mut self, stmt_id: Id>) { + let stmt = self.body.stmts(self.db).get(stmt_id); + match &stmt.kind { + StmtKind::Let { init, .. } => { + if let Some(init) = init { + self.expr(*init); + } + } + StmtKind::Return(expr) => { + if let Some(expr) = expr { + self.expr(*expr); + } + } + StmtKind::Expr(expr) => self.expr(*expr), + StmtKind::Assign { lhs, rhs, .. } => { + self.field_write(stmt_id, *lhs); + self.expr(*rhs); + } + StmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + self.expr(*scrutinee); + } + for arm in arms { + for pat in &arm.pats { + self.pat(*pat); + } + for stmt in &arm.body { + self.stmt(*stmt); + } + } + } + StmtKind::For { + init, + cond, + post, + body, + } => { + for stmt in init { + self.stmt(*stmt); + } + self.expr(*cond); + for stmt in post { + self.stmt(*stmt); + } + for stmt in body { + self.stmt(*stmt); + } + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + self.transforms.push(FrontendTransform::IfStmtToMatch { + body: self.body, + stmt: stmt_id, + origin: SourceOrigin::new(stmt.span, SourceOriginKind::IfStatement), + }); + self.expr(*cond); + for stmt in then_body { + self.stmt(*stmt); + } + if let Some(else_body) = else_body { + for stmt in else_body { + self.stmt(*stmt); + } + } + } + StmtKind::Block { body } => { + for stmt in body { + self.stmt(*stmt); + } + } + StmtKind::Assembly { .. } | StmtKind::Break | StmtKind::Continue | StmtKind::Error => {} + } + } + + fn expr(&mut self, expr_id: Id>) { + let expr = self.body.exprs(self.db).get(expr_id); + if let Some(hir_nameres::Resolution::Field(field)) = + self.expr_resolutions.get(&(self.body, expr_id)) + { + let selector = selector_name(self.db, field); + self.transforms.push(FrontendTransform::FieldRead { + body: self.body, + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::FieldRead), + field: *field, + selector: selector.clone(), + hook: format!("RVA.acc(MemberAccessProxy(ContractStorage(_), {selector}))"), + }); + } + match &expr.kind { + ExprKind::Ident(name) => { + let text = ident_text(self.db, name); + if matches!(text.as_str(), "true" | "false") { + self.transforms.push(FrontendTransform::BoolToUnitSum { + body: self.body, + node: BoolNode::Expr(expr_id), + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + source: text.clone(), + replacement: if text == "true" { "inr(())" } else { "inl(())" }.to_owned(), + }); + } + } + ExprKind::DotCtor { name, args, .. } => { + let text = ident_text(self.db, name); + if matches!(text.as_str(), "true" | "false") { + self.transforms.push(FrontendTransform::BoolToUnitSum { + body: self.body, + node: BoolNode::Expr(expr_id), + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + source: text.clone(), + replacement: if text == "true" { "inr(())" } else { "inl(())" }.to_owned(), + }); + } + for arg in args { + self.expr(*arg); + } + } + ExprKind::Lambda { body, .. } => { + for stmt in body.top_level_stmts(self.db) { + let mut nested = DesugarCollector { + db: self.db, + body: *body, + expr_resolutions: self.expr_resolutions.clone(), + pat_resolutions: self.pat_resolutions.clone(), + call_site_evidence: self.call_site_evidence.clone(), + transforms: Vec::new(), + }; + nested.stmt(*stmt); + self.transforms.extend(nested.transforms); + } + } + ExprKind::BinOp { lhs, rhs, .. } => { + self.expr(*lhs); + self.expr(*rhs); + } + ExprKind::Index { base, index } => { + self.expr(*base); + self.expr(*index); + } + ExprKind::Call { callee, args } => { + if !self.is_direct_call(*callee) { + let evidence = self + .call_site_evidence + .get(&(self.body, expr_id, *callee)) + .cloned(); + let callee_identity = evidence + .as_ref() + .map(|evidence| evidence.callee.clone()) + .unwrap_or(CallSiteCallee::Invokable); + self.transforms.push(FrontendTransform::IndirectCall { + body: self.body, + call_expr: expr_id, + callee_expr: *callee, + origin: SourceOrigin::new(expr.span, SourceOriginKind::IndirectCall), + callee: callee_identity, + args: indirect_arg_shape(args), + evidence, + }); + } + self.expr(*callee); + for arg in args { + self.expr(*arg); + } + } + ExprKind::Field { base, .. } => { + self.expr(*base); + } + ExprKind::Conversion { expr, .. } + | ExprKind::TypeAscription { expr, .. } + | ExprKind::UnaryOp { expr, .. } => self.expr(*expr), + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + self.transforms.push(FrontendTransform::IfExprToMatch { + body: self.body, + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::IfExpression), + }); + self.expr(*cond); + self.expr(*then_expr); + self.expr(*else_expr); + } + ExprKind::Tuple(elems) => { + for elem in elems { + self.expr(*elem); + } + } + ExprKind::Lit(_) | ExprKind::Proxy { .. } | ExprKind::Error => {} + } + } + + fn pat(&mut self, pat_id: Id>) { + let pat = self.body.pats(self.db).get(pat_id); + if let Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::True, + ))) = self.pat_resolutions.get(&(self.body, pat_id)) + { + self.transforms.push(FrontendTransform::BoolToUnitSum { + body: self.body, + node: BoolNode::Pat(pat_id), + origin: SourceOrigin::new(pat.span, SourceOriginKind::BoolConstructor), + source: "true".to_owned(), + replacement: "inr(())".to_owned(), + }); + } + if let Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::False, + ))) = self.pat_resolutions.get(&(self.body, pat_id)) + { + self.transforms.push(FrontendTransform::BoolToUnitSum { + body: self.body, + node: BoolNode::Pat(pat_id), + origin: SourceOrigin::new(pat.span, SourceOriginKind::BoolConstructor), + source: "false".to_owned(), + replacement: "inl(())".to_owned(), + }); + } + match &pat.kind { + PatKind::Ctor { args, .. } | PatKind::Tuple { elems: args } => { + for arg in args { + self.pat(*arg); + } + } + PatKind::ComptimeLabel { expr, .. } => self.expr(*expr), + PatKind::Wildcard | PatKind::Var(_) | PatKind::Lit(_) | PatKind::Error => {} + } + } + + fn field_write(&mut self, stmt_id: Id>, lhs: Id>) { + if let Some(hir_nameres::Resolution::Field(field)) = + self.expr_resolutions.get(&(self.body, lhs)) + { + let selector = selector_name(self.db, field); + let lhs_span = self.body.exprs(self.db).get(lhs).span; + self.transforms.push(FrontendTransform::FieldWrite { + body: self.body, + stmt: stmt_id, + origin: SourceOrigin::new(lhs_span, SourceOriginKind::FieldWrite), + field: *field, + selector: selector.clone(), + hook: format!( + "Assign.assign(LVA.acc(MemberAccessProxy(ContractStorage(_), {selector})), )" + ), + }); + } else { + self.expr(lhs); + } + } + + fn is_direct_call(&self, callee: Id>) -> bool { + self.expr_resolutions + .get(&(self.body, callee)) + .is_some_and(is_direct_call_resolution) + } +} + +fn indirect_arg_shape<'db>(args: &[Id>]) -> IndirectArgShape<'db> { + ProductShape::from_slice(args) +} + +fn body_resolution_for<'a, 'db>( + resolution: &'a hir_nameres::ModuleResolutionMap<'db>, + body: FuncBody<'db>, +) -> Option<&'a hir_nameres::BodyResolutionMap<'db>> { + resolution.bodies.iter().find(|map| { + map.exprs.iter().any(|entry| entry.body == body) + || map.stmt_bindings.iter().any(|entry| entry.body == body) + || map.pats.iter().any(|entry| entry.body == body) + }) +} diff --git a/crates/hir-ty/src/contract/dispatch.rs b/crates/hir-ty/src/contract/dispatch.rs new file mode 100644 index 00000000..511412e0 --- /dev/null +++ b/crates/hir-ty/src/contract/dispatch.rs @@ -0,0 +1,683 @@ +use hir::{ + anchor::DefId, + ast::{ + function::FunctionMutability, + item::{ContractDef, ContractItem, ContractKind, FuncKind, Item, Module}, + }, + diag::{Diagnostic, DiagnosticCode}, + nameres as hir_nameres, + span::Spanned, +}; +use nameres::{LibraryId, module_id_for_source_file}; +use parser::parse_file_to_hir; +use rustc_hash::FxHashMap; + +use super::{ + abi::{ + AbiParam, AbiSelector, AbiSignature, abi_outputs, abi_params, + abi_params_contain_unsupported, abi_selector, abi_type_contains_user_adt, + contract_diag_unsupported_abi_type, method_signature_string, + }, + helpers::{ + find_contract_by_def, function_type_vars, ident_text, lower_normalized_function, + param_names, resolve_contract_item_types, return_names, type_var_bindings, + }, +}; +use crate::{ClassId, ClauseOrigin, Db, PredKind, TraitEnvId, TyCtor, TyKind, UserTyCtorKind}; + +/// Typed dispatch/ABI surface for one contract. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DispatchSurface<'db> { + /// Owning contract definition. + pub contract: DefId<'db>, + /// Contract name. + pub name: String, + /// Externally visible methods eligible for selector dispatch. + pub methods: Vec>, + /// Constructor entry. A missing source constructor is represented as an + /// implicit non-payable unit constructor. + pub constructor: DispatchConstructor, + /// Fallback entry. A missing source fallback is represented as the default + /// non-payable unit fallback. + pub fallback: DispatchFallback<'db>, + /// Canonical-ABI diagnostics produced specifically by the constructor. + /// These remain compilation errors even when a source runtime `main` + /// suppresses generated method dispatch. + pub constructor_abi_diagnostics: Vec, + /// Diagnostics produced while building the surface. + pub diagnostics: Vec, +} + +/// One externally visible method in the dispatch surface. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DispatchMethod<'db> { + /// Function definition. + pub def: DefId<'db>, + /// Source declaration index within the contract. + pub source_index: usize, + /// Source method name. + pub name: String, + /// Explicit source state mutability; absence denotes `nonpayable`. + pub mutability: Option, + /// ABI selector preimage, e.g. `transfer(address,uint256)`. + pub signature: String, + /// First four bytes of `keccak256(signature)`. + pub selector: AbiSelector, + /// ABI input parameters. + pub inputs: Vec, + /// ABI output parameters. + pub outputs: Vec, +} + +/// Constructor dispatch/ABI entry. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum DispatchConstructor { + /// No source constructor: implicit non-payable unit constructor. + Implicit, + /// Source constructor declaration. + Explicit { + /// Source declaration index within the contract. + source_index: usize, + /// Whether deployment may receive value. + payable: bool, + /// ABI input parameters. + inputs: Vec, + }, +} + +/// Fallback dispatch/ABI entry. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum DispatchFallback<'db> { + /// No source fallback: default non-payable unit fallback. + Default, + /// Source fallback declaration. + Explicit { + /// Source fallback definition. + def: DefId<'db>, + /// Source declaration index within the contract. + source_index: usize, + /// Whether fallback calls may receive value. + payable: bool, + /// ABI input parameters. Valid Solcore fallbacks are unit. + inputs: Vec, + /// ABI output parameters. Valid Solcore fallbacks are unit. + outputs: Vec, + }, +} + +/// Returns the typed dispatch surface for one contract in `module`. +pub fn contract_dispatch_surface<'db>( + db: &'db dyn Db, + module: Module<'db>, + contract: ContractDef<'db>, +) -> DispatchSurface<'db> { + let _ = module; + contract_dispatch_surface_by_def(db, contract.def_id_value(db)) +} + +/// Returns the typed dispatch surface for `contract` using the supplied HIR +/// module directly. This is useful for backend-generated HIR overlays whose +/// source file URL may intentionally mirror a user file. +pub fn contract_dispatch_surface_for_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + contract: ContractDef<'db>, +) -> DispatchSurface<'db> { + let item_resolutions = resolve_contract_item_types(db, module); + contract_dispatch_surface_with_resolutions(db, module, &item_resolutions, contract) +} + +#[salsa::tracked] +fn contract_dispatch_surface_by_def<'db>( + db: &'db dyn Db, + contract_def: DefId<'db>, +) -> DispatchSurface<'db> { + let module = parse_file_to_hir(db, contract_def.file(db)).module(db); + let Some(contract) = find_contract_by_def(db, module, contract_def) else { + return DispatchSurface { + contract: contract_def, + name: contract_def + .name(db) + .unwrap_or_else(|| "Contract".to_owned()), + methods: Vec::new(), + constructor: DispatchConstructor::Implicit, + fallback: DispatchFallback::Default, + constructor_abi_diagnostics: Vec::new(), + diagnostics: Vec::new(), + }; + }; + let item_resolutions = resolve_contract_item_types(db, module); + contract_dispatch_surface_with_resolutions(db, module, &item_resolutions, contract) +} + +/// Returns diagnostics for every contract dispatch surface in a module. +pub fn module_contract_diagnostics<'db>(db: &'db dyn Db, module: Module<'db>) -> Vec { + module + .items(db) + .iter() + .filter_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .flat_map(|contract| { + let dispatch_generated = contract_needs_generated_dispatch(db, contract); + let surface = contract_dispatch_surface(db, module, contract); + let mut diagnostics = match contract.kind(db) { + ContractKind::Contract if dispatch_generated => surface.diagnostics, + ContractKind::Contract => { + let mut diagnostics = surface + .diagnostics + .into_iter() + .filter(|diagnostic| diagnostic.code.as_deref() != Some("SC0231")) + .collect::>(); + diagnostics.extend(surface.constructor_abi_diagnostics); + diagnostics + } + ContractKind::Interface | ContractKind::Library => surface.diagnostics, + }; + if contract.kind(db) == ContractKind::Contract { + diagnostics.extend(contract_runtime_main_diagnostics(db, contract)); + } + diagnostics + }) + .filter(|diagnostic| { + matches!( + diagnostic.code.as_deref(), + Some("SC0230" | "SC0231" | "SC0232" | "SC0233" | "SC0235" | "SC0236") + ) + }) + .collect() +} + +pub(crate) fn module_manual_generic_abi_diagnostics<'db>( + db: &'db dyn Db, + module: Module<'db>, + trait_env: TraitEnvId<'db>, +) -> Vec { + let manual_evidence = trait_env + .clauses(db) + .into_iter() + .filter_map(|clause| { + let ClauseOrigin::Instance { def: instance, .. } = clause.origin else { + return None; + }; + if instance + .fingerprint(db) + .as_deref() + .is_some_and(|fingerprint| { + fingerprint.starts_with("solcore.generated.std_dispatch.") + }) + { + return None; + } + if module_id_for_source_file(db, instance.file(db)) + .is_some_and(|module| matches!(module.library(db), LibraryId::Std)) + { + return None; + } + let PredKind::InClass { + class: ClassId::User(class), + main, + .. + } = clause.head.kind(db) + else { + return None; + }; + let class_name = canonical_abi_class_name(db, *class)?; + let generic_adt = if class_name == "Generic" { + match main.kind(db) { + TyKind::Named { + ctor: TyCtor::User(user), + .. + } if user.kind == UserTyCtorKind::Adt => Some(user.def), + _ => return None, + } + } else { + None + }; + Some((instance, class_name, generic_adt)) + }) + .collect::>(); + if manual_evidence.is_empty() { + return Vec::new(); + } + + let item_resolutions = resolve_contract_item_types(db, module); + let mut diagnostics = Vec::new(); + for item in module.items(db) { + let Item::ContractDef(contract) = *item else { + continue; + }; + if contract.kind(db) == ContractKind::Library { + continue; + } + let dispatch_generated = contract_needs_generated_dispatch(db, contract); + let contract_name = ident_text(db, &contract.name_elem(db)); + let contract_type_vars = + type_var_bindings(contract.def_id_value(db), contract.ty_param_elems(db)); + for item in contract.items(db) { + let ContractItem::FunctionDef(function) = *item else { + continue; + }; + let sig = function.sig(db); + let abi_context = match function.kind(db) { + FuncKind::Constructor if contract.kind(db) == ContractKind::Contract => { + Some("constructor".to_owned()) + } + FuncKind::Function + if sig.is_abi_visible() + && (dispatch_generated || contract.kind(db) == ContractKind::Interface) => + { + Some(format!( + "function `{}` declared {}", + ident_text(db, &sig.name), + sig.visibility_kind() + .expect("ABI-visible function has explicit visibility") + .keyword() + )) + } + FuncKind::Constructor | FuncKind::Function | FuncKind::Fallback => None, + }; + let Some(abi_context) = abi_context else { + continue; + }; + let type_vars = + function_type_vars(db, &contract_type_vars, function.def_id_value(db), sig); + let lowered = lower_normalized_function( + db, + module, + &item_resolutions, + contract.def_id_value(db), + function, + &type_vars, + ); + let mut exposed_tys = lowered.params.clone(); + if function.kind(db) == FuncKind::Function { + exposed_tys.push(lowered.ret); + } + for (instance, class_name, generic_adt) in &manual_evidence { + if generic_adt.is_some_and(|adt| { + !exposed_tys + .iter() + .any(|ty| abi_type_contains_user_adt(db, *ty, adt)) + }) { + continue; + } + let subject = generic_adt.map_or_else( + || format!("visible manual `{class_name}` evidence"), + |adt| { + let adt_name = adt.name(db).unwrap_or_else(|| "".to_owned()); + format!("`{adt_name}` with visible manual `Generic` evidence") + }, + ); + diagnostics.push( + Diagnostic::error(format!( + "{abi_context} ABI for {} `{contract_name}` cannot use {subject}", + contract.kind(db).keyword() + )) + .with_code("SC0231") + .with_primary_label( + db, + sig.span, + Some("external ABI evidence must be compiler-owned and canonical"), + ) + .with_note(format!( + "impl `{}` can override canonical `{class_name}` behavior", + instance + .name(db) + .unwrap_or_else(|| class_name.to_string()) + )) + .with_help( + "remove the visible manual ABI impl or keep this declaration out of the external ABI", + ), + ); + } + } + } + diagnostics +} + +fn canonical_abi_class_name(db: &dyn Db, class: DefId<'_>) -> Option<&'static str> { + let name = class.name(db)?; + let module = module_id_for_source_file(db, class.file(db))?; + if module.library(db) != &LibraryId::Std { + return None; + } + match (module.logical_path(db).as_slice(), name.as_str()) { + ([path], "Generic" | "ABIAttribs" | "ABIEncode" | "ABIDecode") if path == "std" => { + match name.as_str() { + "Generic" => Some("Generic"), + "ABIAttribs" => Some("ABIAttribs"), + "ABIEncode" => Some("ABIEncode"), + "ABIDecode" => Some("ABIDecode"), + _ => None, + } + } + ([path], "SigString") if path == "dispatch" => Some("SigString"), + _ => None, + } +} + +fn contract_runtime_main_diagnostics<'db>( + db: &'db dyn Db, + contract: ContractDef<'db>, +) -> Vec { + contract + .items(db) + .iter() + .filter_map(|item| { + let ContractItem::FunctionDef(function) = *item else { + return None; + }; + let sig = function.sig(db); + (function.kind(db) == FuncKind::Function + && ident_text(db, &sig.name) == "main" + && !sig.params.atom().is_empty()) + .then(|| { + Diagnostic::error("contract runtime `main` must not take parameters") + .with_code(DiagnosticCode::TYPECK_CONTRACT_RUNTIME_MAIN_ARITY) + .with_primary_label( + db, + sig.params.span(db), + Some("runtime entry is called without arguments"), + ) + .with_help("remove the parameters or rename this function") + }) + }) + .collect() +} + +/// Returns whether the compiler must synthesize this contract's runtime entry. +/// +/// This deliberately follows the language's existing/Haskell-compatible +/// convention: any contract-local ordinary function named `main` is a +/// user-supplied runtime entry, irrespective of visibility. +pub fn contract_needs_generated_dispatch<'db>(db: &'db dyn Db, contract: ContractDef<'db>) -> bool { + contract.kind(db) == ContractKind::Contract && !contract.has_runtime_main(db) +} + +fn contract_dispatch_surface_with_resolutions<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + contract: ContractDef<'db>, +) -> DispatchSurface<'db> { + let contract_name = ident_text(db, &contract.name_elem(db)); + let contract_type_vars = + type_var_bindings(contract.def_id_value(db), contract.ty_param_elems(db)); + let mut diagnostics = Vec::new(); + let mut methods = Vec::new(); + let mut constructor: Option = None; + let mut constructor_abi_diagnostics = Vec::new(); + let mut fallback: Option> = None; + + for (source_index, item) in contract.items(db).iter().enumerate() { + let ContractItem::FunctionDef(function) = *item else { + continue; + }; + match function.kind(db) { + FuncKind::Function => { + let sig = function.sig(db); + if !sig.is_abi_visible() || ident_text(db, &sig.name) == "fallback" { + continue; + } + let type_vars = + function_type_vars(db, &contract_type_vars, function.def_id_value(db), sig); + let lowered = lower_normalized_function( + db, + module, + item_resolutions, + contract.def_id_value(db), + function, + &type_vars, + ); + let param_names = param_names(db, sig.params.atom()); + let inputs = abi_params( + db, + ¶m_names, + &lowered.params, + &mut diagnostics, + sig.span, + ); + let outputs = abi_outputs( + db, + &return_names(db, sig), + lowered.ret, + &mut diagnostics, + sig.span, + ); + let signature = + method_signature_string(db, &ident_text(db, &sig.name), &lowered.params) + .unwrap_or_else(|err| { + diagnostics.push(contract_diag_unsupported_abi_type( + db, + sig.span, + &ident_text(db, &sig.name), + &err, + )); + format!("{}()", ident_text(db, &sig.name)) + }); + let selector = abi_selector(db, AbiSignature::new(db, signature.clone())); + methods.push(DispatchMethod { + def: function.def_id_value(db), + source_index, + name: ident_text(db, &sig.name), + mutability: sig.mutability_kind(), + signature, + selector, + inputs, + outputs, + }); + } + FuncKind::Constructor => { + if constructor.is_some() { + diagnostics.push(contract_diag_multiple_constructors(db, function.span(db))); + continue; + } + let sig = function.sig(db); + let type_vars = + function_type_vars(db, &contract_type_vars, function.def_id_value(db), sig); + let lowered = lower_normalized_function( + db, + module, + item_resolutions, + contract.def_id_value(db), + function, + &type_vars, + ); + let inputs = abi_params( + db, + ¶m_names(db, sig.params.atom()), + &lowered.params, + &mut constructor_abi_diagnostics, + sig.span, + ); + diagnostics.extend(constructor_abi_diagnostics.iter().cloned()); + constructor = Some(DispatchConstructor::Explicit { + source_index, + payable: sig.is_payable(), + inputs, + }); + } + FuncKind::Fallback => { + if fallback.is_some() { + diagnostics.push(contract_diag_multiple_fallbacks(db, function.span(db))); + continue; + } + let sig = function.sig(db); + let type_vars = + function_type_vars(db, &contract_type_vars, function.def_id_value(db), sig); + let lowered = lower_normalized_function( + db, + module, + item_resolutions, + contract.def_id_value(db), + function, + &type_vars, + ); + let inputs = abi_params( + db, + ¶m_names(db, sig.params.atom()), + &lowered.params, + &mut diagnostics, + sig.span, + ); + let outputs = abi_outputs( + db, + &return_names(db, sig), + lowered.ret, + &mut diagnostics, + sig.span, + ); + if !inputs.is_empty() || !outputs.is_empty() { + diagnostics.push(contract_diag_unsupported_fallback_shape( + db, + function.span(db), + )); + } + fallback = Some(DispatchFallback::Explicit { + def: function.def_id_value(db), + source_index, + payable: sig.is_payable(), + inputs, + outputs, + }); + } + } + } + + let constructor = constructor.unwrap_or(DispatchConstructor::Implicit); + let fallback = fallback.unwrap_or(DispatchFallback::Default); + + let mut seen_signatures = FxHashMap::::default(); + let mut seen_selectors = FxHashMap::::default(); + for (method_index, method) in methods.iter().enumerate() { + if abi_params_contain_unsupported(&method.inputs) { + continue; + } + + if let Some(&previous_index) = seen_signatures.get(&method.signature) { + diagnostics.push(contract_diag_duplicate_signature( + db, + dispatch_method_span(db, contract, method), + dispatch_method_span(db, contract, &methods[previous_index]), + contract.kind(db), + &contract_name, + &method.signature, + )); + } else { + seen_signatures.insert(method.signature.clone(), method_index); + } + + if let Some(&previous_index) = seen_selectors.get(&method.selector) { + let previous = &methods[previous_index]; + if previous.signature != method.signature { + diagnostics.push(contract_diag_selector_collision( + db, + dispatch_method_span(db, contract, method), + dispatch_method_span(db, contract, previous), + contract.kind(db), + &contract_name, + method, + previous, + )); + } + } else { + seen_selectors.insert(method.selector, method_index); + } + } + + DispatchSurface { + contract: contract.def_id_value(db), + name: contract_name, + methods, + constructor, + fallback, + constructor_abi_diagnostics, + diagnostics, + } +} + +fn contract_diag_duplicate_signature<'db>( + db: &'db dyn Db, + current_span: hir::span::Span<'db>, + previous_span: hir::span::Span<'db>, + declaration_kind: ContractKind, + declaration_name: &str, + signature: &str, +) -> Diagnostic { + Diagnostic::error(format!( + "duplicate external ABI signature in {} `{declaration_name}`: {signature}", + declaration_kind.keyword() + )) + .with_code("SC0230") + .with_primary_label(db, current_span, Some("duplicate ABI signature")) + .with_secondary_label(db, previous_span, Some("previous declaration")) +} + +fn contract_diag_selector_collision<'db>( + db: &'db dyn Db, + current_span: hir::span::Span<'db>, + previous_span: hir::span::Span<'db>, + declaration_kind: ContractKind, + declaration_name: &str, + current: &DispatchMethod<'db>, + previous: &DispatchMethod<'db>, +) -> Diagnostic { + Diagnostic::error(format!( + "external ABI selector collision in {} `{declaration_name}`: `{}` and `{}` both use {}", + declaration_kind.keyword(), + previous.signature, + current.signature, + current.selector.to_hex(), + )) + .with_code(DiagnosticCode::TYPECK_CONTRACT_SELECTOR_COLLISION) + .with_primary_label( + db, + current_span, + Some(format!("`{}` collides here", current.signature)), + ) + .with_secondary_label( + db, + previous_span, + Some(format!("`{}` first used this selector", previous.signature)), + ) +} + +fn dispatch_method_span<'db>( + db: &'db dyn Db, + contract: ContractDef<'db>, + method: &DispatchMethod<'db>, +) -> hir::span::Span<'db> { + match contract.items(db).get(method.source_index) { + Some(ContractItem::FunctionDef(function)) => function.sig(db).span, + _ => contract.name_elem(db).span(db), + } +} + +fn contract_diag_multiple_constructors<'db>( + db: &'db dyn Db, + span: hir::span::Span<'db>, +) -> Diagnostic { + Diagnostic::error("contract has more than one constructor") + .with_code("SC0232") + .with_primary_label(db, span, Some("extra constructor")) +} + +fn contract_diag_multiple_fallbacks<'db>( + db: &'db dyn Db, + span: hir::span::Span<'db>, +) -> Diagnostic { + Diagnostic::error("contract has more than one fallback") + .with_code("SC0233") + .with_primary_label(db, span, Some("extra fallback")) +} + +fn contract_diag_unsupported_fallback_shape<'db>( + db: &'db dyn Db, + span: hir::span::Span<'db>, +) -> Diagnostic { + Diagnostic::error("fallback ABI must have type `function()`") + .with_code("SC0231") + .with_primary_label(db, span, Some("unsupported fallback ABI")) +} diff --git a/crates/hir-ty/src/contract/helpers.rs b/crates/hir-ty/src/contract/helpers.rs new file mode 100644 index 00000000..114af949 --- /dev/null +++ b/crates/hir-ty/src/contract/helpers.rs @@ -0,0 +1,114 @@ +pub(super) use hir::nameres::{ident_text, type_var_bindings}; +use hir::{ + Db as HirDb, + anchor::DefId, + ast::{ + function::FuncParam, + item::{ContractDef, FunctionDef, Item, Module}, + }, + nameres as hir_nameres, + nameres::param_bindings, +}; +use nameres::module_id_for_source_file; + +use crate::{Db, LoweredFunction, lower_normalized_function_with_inferred_signature}; + +pub(super) fn lower_normalized_function<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + enclosing_contract: DefId<'db>, + function: FunctionDef<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> LoweredFunction<'db> { + let body_map = function.body(db).map(|body| { + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract: Some(enclosing_contract), + params: param_bindings(function.sig(db).params.atom()), + type_vars: type_vars.to_vec(), + }; + hir_nameres::resolve_body(db, body, context) + }); + lower_normalized_function_with_inferred_signature( + db, + module, + item_resolutions, + function, + type_vars, + body_map.as_ref(), + None, + ) +} + +pub(super) fn resolve_contract_item_types<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> hir_nameres::ItemResolutionFacts<'db> { + let file = module.def_id_value(db).file(db); + let Some(module_id) = module_id_for_source_file(db, file) else { + return hir_nameres::resolve_item_type_facts(db, module); + }; + let env = nameres::module_import_surface(db, module_id); + let Some(item_scope) = env.item_scope.as_ref() else { + return hir_nameres::resolve_item_type_facts(db, module); + }; + hir_nameres::resolve_item_type_facts_with_imports(db, module, item_scope, &env) +} + +pub(super) fn find_contract_by_def<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(*contract), + _ => None, + }) +} + +pub(super) fn selector_name<'db>(db: &'db dyn HirDb, field: &hir_nameres::FieldId<'db>) -> String { + let contract = field + .contract + .name(db) + .unwrap_or_else(|| "Contract".to_owned()); + format!("{contract}_field{}_sel", field.index.as_u32()) +} + +pub(super) fn function_type_vars<'db>( + db: &'db dyn HirDb, + inherited: &[hir_nameres::TypeVarBinding<'db>], + owner: DefId<'db>, + sig: &hir::ast::function::FuncSig<'db>, +) -> Vec> { + let mut vars = inherited.to_vec(); + vars.extend(type_var_bindings(owner, &sig.type_vars)); + let _ = db; + vars +} + +pub(super) fn param_names<'db>(db: &'db dyn HirDb, params: &[FuncParam<'db>]) -> Vec { + params + .iter() + .filter_map(|param| match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some(ident_text(db, name)) + } + FuncParam::Error { .. } => None, + }) + .collect() +} + +pub(super) fn return_names<'db>( + db: &'db dyn HirDb, + sig: &hir::ast::function::FuncSig<'db>, +) -> Vec { + sig.ret_names + .iter() + .map(|name| { + name.as_ref() + .map(|name| ident_text(db, name)) + .unwrap_or_default() + }) + .collect() +} diff --git a/crates/hir-ty/src/contract/mod.rs b/crates/hir-ty/src/contract/mod.rs new file mode 100644 index 00000000..a20f02e4 --- /dev/null +++ b/crates/hir-ty/src/contract/mod.rs @@ -0,0 +1,26 @@ +//! Contract-specific typed surfaces and frontend desugar planning. +//! +//! This module intentionally lives in `hir-ty`, not a new `hir-lower` crate: +//! dispatch eligibility, ABI spelling, duplicate public signatures, and field +//! initializer checks all need resolved names and lowered semantic types. The +//! later Hull/codegen stages can consume the typed surface and storage hooks +//! without re-deriving frontend rules from raw HIR. + +mod abi; +mod abi_json; +mod desugar; +mod dispatch; +mod helpers; + +pub use abi::{AbiParam, AbiSelector, AbiSignature, AbiType, abi_selector}; +pub use abi_json::contract_abi_json; +pub use desugar::{ + BodyDesugarPlan, BoolNode, FrontendDesugarPlan, FrontendTransform, IndirectArgShape, + frontend_desugar_plan, +}; +pub(crate) use dispatch::module_manual_generic_abi_diagnostics; +pub use dispatch::{ + DispatchConstructor, DispatchFallback, DispatchMethod, DispatchSurface, + contract_dispatch_surface, contract_dispatch_surface_for_module, + contract_needs_generated_dispatch, module_contract_diagnostics, +}; diff --git a/crates/hir-ty/src/coverage.rs b/crates/hir-ty/src/coverage.rs new file mode 100644 index 00000000..24144cd8 --- /dev/null +++ b/crates/hir-ty/src/coverage.rs @@ -0,0 +1,51 @@ +//! Solcore-specific constructor heads used by pattern-match coverage analysis. +//! +//! The generic usefulness algorithm lives in `matchcov`; this module only +//! defines the semantic identities supplied by the type-checker adapter. + +use hir::{anchor::DefId, nameres::CtorIndex}; + +/// A semantic pattern head supplied to `matchcov`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CoverageHead<'db> { + /// A finite language constructor. + Ctor(CoverageCtor<'db>), + /// A canonical literal in an open constructor domain. + Literal(String), +} + +/// The shared pattern representation specialized to Solcore heads. +pub(crate) type CoveragePat<'db> = matchcov::Pattern>; + +/// A finite constructor known to the Solcore type system. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CoverageCtor<'db> { + /// User ADT constructor, identified independently of its display name. + User { + /// Type definition that owns this constructor. + ty: DefId<'db>, + /// Constructor index inside the ADT definition. + index: CtorIndex, + }, + /// Builtin constructor. + Builtin(BuiltinCoverageCtor), +} + +/// Builtin constructor heads known to the coverage adapter. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BuiltinCoverageCtor { + /// Boolean `true`. + True, + /// Boolean `false`. + False, + /// Unit constructor. + Unit, + /// Tuple constructor of the given arity. + Tuple(usize), + /// Builtin pair constructor. + Pair, + /// Builtin sum left injection. + Inl, + /// Builtin sum right injection. + Inr, +} diff --git a/crates/hir-ty/src/desugar.rs b/crates/hir-ty/src/desugar.rs new file mode 100644 index 00000000..48072a1a --- /dev/null +++ b/crates/hir-ty/src/desugar.rs @@ -0,0 +1,1167 @@ +//! Cache-stable pre-typecheck desugar planning. +//! +//! This module records desugar facts without mutating parsed HIR or reparsing +//! generated source. That keeps the parser and LSP-facing spans tied to the +//! user's file while type checking and specialization can opt into a normalized +//! view. +//! +//! Plans in this module are the input view for type checking: tuple syntax, +//! bool constructors, and `if` forms are interpreted as their core product, +//! sum, or match shapes before inference. Backend-facing rewrites that require +//! type-class evidence, ABI surface knowledge, or storage hooks belong in +//! `contract::desugar` instead. + +use hir::{ + anchor::DefId, + arena::{Arena, Id}, + ast::{ + function::{Expr, ExprKind, FuncBody, FuncParam, FuncSig, Pat, PatKind, Stmt, StmtKind}, + item::{ + AdtDef, ClassDef, ContractDef, ContractItem, FieldDef, FieldInit, FunctionDef, + InstanceDef, Item, Module, TypeAlias, + }, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + span::{Span, Spanned}, +}; + +use crate::Db; + +/// Source span that should receive diagnostics for a generated/desugared node. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct SourceOrigin<'db> { + /// User-written syntax that caused the generated node to exist. + pub span: Span<'db>, + /// Desugar category for diagnostics and debugging. + pub kind: SourceOriginKind, +} + +impl<'db> SourceOrigin<'db> { + /// Creates a source-origin record for generated/desugared syntax. + pub const fn new(span: Span<'db>, kind: SourceOriginKind) -> Self { + Self { span, kind } + } +} + +/// Categories of user syntax that can produce generated/desugared nodes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum SourceOriginKind { + /// Tuple expression syntax normalized to unit/single/pair product form. + TupleExpr, + /// Tuple pattern syntax normalized to unit/single/pair product form. + TuplePat, + /// Tuple type syntax normalized to unit/single/pair product form. + TupleType, + /// If statement lowered through match-on-bool. + IfStatement, + /// If expression lowered through match-on-bool. + IfExpression, + /// Bool constructor/pattern rewritten to the unit-sum encoding. + BoolConstructor, + /// Contract field read rewritten to a storage hook. + FieldRead, + /// Contract field write rewritten to a storage hook. + FieldWrite, + /// Indirect call rewritten through the invokable dictionary. + IndirectCall, + /// Generated glue with no tighter user token than the enclosing construct. + GeneratedGlue, +} + +/// Product payload shape used by frontend desugaring. +/// +/// Tuple-like syntax is normalized with this language-level product convention: +/// +/// - `()` becomes unit. +/// - `(a)` becomes `a`. +/// - `(a, b)` becomes `pair(a, b)`. +/// - `(a, b, c)` becomes `pair(a, pair(b, c))`. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ProductShape { + /// Empty product/unit. + Unit, + /// Singleton product, represented without a pair wrapper. + Single(T), + /// Right-nested pair product. + Pair { + /// First element at this level. + head: T, + /// Remaining product payload. + tail: Box>, + }, +} + +impl ProductShape { + /// Builds a right-nested product shape from source-order elements. + pub fn from_slice(elems: &[T]) -> Self { + let Some((head, tail)) = elems.split_first() else { + return Self::Unit; + }; + if tail.is_empty() { + Self::Single(head.clone()) + } else { + Self::Pair { + head: head.clone(), + tail: Box::new(Self::from_slice(tail)), + } + } + } + + /// Returns the number of source-order elements represented by this shape. + pub fn len(&self) -> usize { + match self { + Self::Unit => 0, + Self::Single(_) => 1, + Self::Pair { tail, .. } => 1 + tail.len(), + } + } + + /// Returns whether this shape has no elements. + pub fn is_empty(&self) -> bool { + matches!(self, Self::Unit) + } + + /// Flattens the product shape back into source-order elements. + pub fn to_vec(&self) -> Vec { + let mut out = Vec::new(); + self.extend_vec(&mut out); + out + } + + fn extend_vec(&self, out: &mut Vec) { + match self { + Self::Unit => {} + Self::Single(elem) => out.push(elem.clone()), + Self::Pair { head, tail } => { + out.push(head.clone()); + tail.extend_vec(out); + } + } + } + + /// Maps every element while preserving the product shape. + pub fn map(&self, f: &mut F) -> ProductShape + where + F: FnMut(&T) -> U, + { + match self { + Self::Unit => ProductShape::Unit, + Self::Single(elem) => ProductShape::Single(f(elem)), + Self::Pair { head, tail } => ProductShape::Pair { + head: f(head), + tail: Box::new(tail.map(f)), + }, + } + } +} + +/// Read-only desugar view for a root body and nested lambda bodies. +/// +/// This is intentionally a view over tracked plan data rather than rewritten +/// HIR. Consumers can opt into normalized source shapes while diagnostics and +/// LSP-facing spans continue to point at user-written syntax. +pub struct BodyDesugarView<'a, 'db> { + plans: &'a [BodyPreTypeckDesugarPlan<'db>], +} + +/// Match-shaped view of an if statement. +pub struct IfStmtMatchView<'a, 'db> { + /// User syntax that produced this view. + pub origin: SourceOrigin<'db>, + /// Boolean scrutinee. + pub cond: Id>, + /// Statements in the true branch. + pub then_body: &'a [Id>], + /// Statements in the false branch, when present. + pub else_body: Option<&'a [Id>]>, +} + +/// Match-shaped view of an if expression. +pub struct IfExprMatchView<'db> { + /// User syntax that produced this view. + pub origin: SourceOrigin<'db>, + /// Boolean scrutinee. + pub cond: Id>, + /// Expression in the true branch. + pub then_expr: Id>, + /// Expression in the false branch. + pub else_expr: Id>, +} + +/// Unit-sum view of a bool constructor or pattern. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BoolUnitSumView<'db> { + /// User syntax that produced this view. + pub origin: SourceOrigin<'db>, + /// Boolean constructor value. + pub value: bool, +} + +impl BoolUnitSumView<'_> { + /// Source constructor spelling. + pub const fn source(self) -> &'static str { + if self.value { "true" } else { "false" } + } + + /// Unit-sum replacement spelling. + pub const fn replacement(self) -> &'static str { + if self.value { "inr(())" } else { "inl(())" } + } +} + +impl<'a, 'db> BodyDesugarView<'a, 'db> { + /// Creates a view over pre-typecheck body desugar plans. + pub fn new(plans: &'a [BodyPreTypeckDesugarPlan<'db>]) -> Self { + Self { plans } + } + + /// Returns the planned product shape for a tuple expression, if present. + pub fn tuple_expr_product( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option<&'a ProductShape>>> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::TupleExprToProduct { + expr: candidate, + product, + .. + } if *candidate == expr => Some(product), + _ => None, + }) + } + + /// Returns the planned product shape for a tuple pattern, if present. + pub fn tuple_pat_product( + &self, + body: FuncBody<'db>, + pat: Id>, + ) -> Option<&'a ProductShape>>> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::TuplePatToProduct { + pat: candidate, + product, + .. + } if *candidate == pat => Some(product), + _ => None, + }) + } + + /// Returns the user origin for a tuple expression transform. + pub fn tuple_expr_origin( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + self.expr_origin(body, expr) + .filter(|origin| origin.kind == SourceOriginKind::TupleExpr) + } + + /// Returns the user origin for an expression transform. + pub fn expr_origin( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::TupleExprToProduct { + expr: candidate, + origin, + .. + } if *candidate == expr => Some(*origin), + PreTypeckTransform::IfExprToMatch { + expr: candidate, + origin, + .. + } if *candidate == expr => Some(*origin), + PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Expr(candidate), + origin, + .. + } if *candidate == expr => Some(*origin), + _ => None, + }) + } + + /// Returns the planned match-shaped view for an if statement. + pub fn if_stmt_match( + &self, + body: FuncBody<'db>, + stmt: Id>, + ) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::IfStmtToMatch { + stmt: candidate, + origin, + cond, + then_body, + else_body, + } if *candidate == stmt => Some(IfStmtMatchView { + origin: *origin, + cond: *cond, + then_body, + else_body: else_body.as_deref(), + }), + _ => None, + }) + } + + /// Returns the planned match-shaped view for an if expression. + pub fn if_expr_match( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::IfExprToMatch { + expr: candidate, + origin, + cond, + then_expr, + else_expr, + } if *candidate == expr => Some(IfExprMatchView { + origin: *origin, + cond: *cond, + then_expr: *then_expr, + else_expr: *else_expr, + }), + _ => None, + }) + } + + /// Returns the user origin for a statement transform. + pub fn stmt_origin( + &self, + body: FuncBody<'db>, + stmt: Id>, + ) -> Option> { + self.if_stmt_match(body, stmt).map(|view| view.origin) + } + + /// Returns the user origin for a tuple pattern transform. + pub fn tuple_pat_origin( + &self, + body: FuncBody<'db>, + pat: Id>, + ) -> Option> { + self.pat_origin(body, pat) + .filter(|origin| origin.kind == SourceOriginKind::TuplePat) + } + + /// Returns the user origin for a pattern transform. + pub fn pat_origin(&self, body: FuncBody<'db>, pat: Id>) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::TuplePatToProduct { + pat: candidate, + origin, + .. + } if *candidate == pat => Some(*origin), + PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Pat(candidate), + origin, + .. + } if *candidate == pat => Some(*origin), + _ => None, + }) + } + + /// Returns the planned unit-sum view for a bool expression constructor. + pub fn bool_expr_unit_sum( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Expr(candidate), + origin, + value, + } if *candidate == expr => Some(BoolUnitSumView { + origin: *origin, + value: *value, + }), + _ => None, + }) + } + + /// Returns the planned unit-sum view for a bool pattern constructor. + pub fn bool_pat_unit_sum( + &self, + body: FuncBody<'db>, + pat: Id>, + ) -> Option> { + self.body_plan(body)? + .transforms + .iter() + .find_map(|transform| match transform { + PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Pat(candidate), + origin, + value, + } if *candidate == pat => Some(BoolUnitSumView { + origin: *origin, + value: *value, + }), + _ => None, + }) + } + + /// Returns the planned product shape for a tuple type reference. + pub fn tuple_type_product(&self, ty: TypeRef<'db>) -> Option<&'a ProductShape>> { + self.plans + .iter() + .flat_map(|plan| &plan.types) + .find_map(|desugar| (desugar.ty == ty).then_some(&desugar.product)) + } + + /// Returns the user origin for a type transform. + pub fn type_origin(&self, ty: TypeRef<'db>) -> Option> { + self.plans + .iter() + .flat_map(|plan| &plan.types) + .find_map(|desugar| (desugar.ty == ty).then_some(desugar.origin)) + } + + fn body_plan(&self, body: FuncBody<'db>) -> Option<&'a BodyPreTypeckDesugarPlan<'db>> { + self.plans.iter().find(|plan| plan.body == body) + } +} + +/// Pre-typecheck input view for one module. +/// +/// This plan is consumed before or during type checking. It deliberately avoids +/// backend-only hooks so each body can keep depending on the smallest possible +/// tracked desugar tree. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct PreTypeckDesugarPlan<'db> { + /// Tuple type references from item signatures, aliases, and fields. + /// + /// Body-local conversion target types live in [`BodyPreTypeckDesugarPlan::types`] + /// so type checking can depend on one body at a time. + pub types: Vec>, + /// Type-checker input views inside function and lambda bodies. + pub bodies: Vec>, + /// Type-checker input views inside contract field initializers. + pub field_inits: Vec>, +} + +/// Planned tuple-type desugar for one source type occurrence. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct TypeProductDesugar<'db> { + /// Source type reference being normalized. + pub ty: TypeRef<'db>, + /// Diagnostic origin for the normalized type. + pub origin: SourceOrigin<'db>, + /// Unit/single/right-nested-pair product shape. + pub product: ProductShape>, +} + +/// Body-local pre-typecheck input view. +/// +/// The source body is left unchanged; consumers query this plan through +/// [`BodyDesugarView`] to interpret selected source nodes as core syntax. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyPreTypeckDesugarPlan<'db> { + /// Function or lambda body containing the source nodes. + pub body: FuncBody<'db>, + /// Tuple type references inside local annotations and lambda signatures. + pub types: Vec>, + /// Expression/pattern/statement transforms in traversal order. + pub transforms: Vec>, +} + +/// One body-local pre-typecheck transform. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum PreTypeckTransform<'db> { + /// Tuple expression normalized to unit/single/pair product form. + TupleExprToProduct { + /// Source expression. + expr: Id>, + /// Diagnostic origin for generated product nodes. + origin: SourceOrigin<'db>, + /// Unit/single/right-nested-pair payload shape. + product: ProductShape>>, + }, + /// Tuple pattern normalized to unit/single/pair product form. + TuplePatToProduct { + /// Source pattern. + pat: Id>, + /// Diagnostic origin for generated product nodes. + origin: SourceOrigin<'db>, + /// Unit/single/right-nested-pair payload shape. + product: ProductShape>>, + }, + /// If statement viewed as a match over the boolean condition. + IfStmtToMatch { + /// Source statement. + stmt: Id>, + /// Diagnostic origin for generated match nodes. + origin: SourceOrigin<'db>, + /// Boolean condition expression. + cond: Id>, + /// Statements in the true branch. + then_body: Vec>>, + /// Statements in the false branch, when present. + else_body: Option>>>, + }, + /// If expression viewed as a match over the boolean condition. + IfExprToMatch { + /// Source expression. + expr: Id>, + /// Diagnostic origin for generated match nodes. + origin: SourceOrigin<'db>, + /// Boolean condition expression. + cond: Id>, + /// Expression in the true branch. + then_expr: Id>, + /// Expression in the false branch. + else_expr: Id>, + }, + /// Bool constructor or pattern viewed as a unit-sum constructor. + BoolToUnitSum { + /// Source node. + node: BoolUnitSumNode<'db>, + /// Diagnostic origin for generated unit-sum nodes. + origin: SourceOrigin<'db>, + /// Source boolean value. + value: bool, + }, +} + +/// Bool source node category for unit-sum views. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BoolUnitSumNode<'db> { + /// Expression constructor. + Expr(Id>), + /// Pattern constructor. + Pat(Id>), +} + +/// Field-initializer pre-typecheck input view. +/// +/// Field initializers are typechecked through synthetic bodies, so this module +/// also records a compact module-level view for diagnostics and tests. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct FieldInitPreTypeckDesugarPlan<'db> { + /// Contract owning the field. + pub contract: DefId<'db>, + /// Field name for debugging and snapshot-friendly assertions. + pub field_name: String, + /// Tuple expression transforms in traversal order. + pub transforms: Vec>, +} + +/// One field-initializer pre-typecheck transform. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum FieldInitPreTypeckTransform<'db> { + /// Tuple expression normalized to unit/single/pair product form. + TupleExprToProduct { + /// Source expression in the field initializer arena. + expr: Id>, + /// Diagnostic origin for generated product nodes. + origin: SourceOrigin<'db>, + /// Unit/single/right-nested-pair payload shape. + product: ProductShape>>, + }, + /// If expression viewed as a match over the boolean condition. + IfExprToMatch { + /// Source expression in the field initializer arena. + expr: Id>, + /// Diagnostic origin for generated match nodes. + origin: SourceOrigin<'db>, + /// Boolean condition expression. + cond: Id>, + /// Expression in the true branch. + then_expr: Id>, + /// Expression in the false branch. + else_expr: Id>, + }, + /// Bool constructor viewed as a unit-sum constructor. + BoolToUnitSum { + /// Source expression in the field initializer arena. + expr: Id>, + /// Diagnostic origin for generated unit-sum nodes. + origin: SourceOrigin<'db>, + /// Source boolean value. + value: bool, + }, +} + +/// Computes pre-typecheck desugar facts for the parsed module without changing +/// the module itself. +#[salsa::tracked] +pub fn pre_typeck_desugar_plan<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> PreTypeckDesugarPlan<'db> { + let mut collector = ModuleCollector { + db, + types: Vec::new(), + bodies: Vec::new(), + field_inits: Vec::new(), + }; + for item in module.items(db) { + collector.item(*item); + } + PreTypeckDesugarPlan { + types: collector.types, + bodies: collector.bodies, + field_inits: collector.field_inits, + } +} + +/// Computes pre-typecheck desugar facts for one body tree. +/// +/// The returned list contains `body` and any nested lambda bodies that contain +/// tuple/product desugar facts. Keeping this as a separate tracked query gives +/// type checking a narrow cache boundary to call before inference. +#[salsa::tracked] +pub fn pre_typeck_desugar_body_tree<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, +) -> Vec> { + let mut collector = BodyCollector { + db, + body, + types: Vec::new(), + nested_bodies: Vec::new(), + transforms: Vec::new(), + }; + for stmt in body.top_level_stmts(db) { + collector.stmt(*stmt); + } + + let mut bodies = Vec::new(); + if !collector.types.is_empty() || !collector.transforms.is_empty() { + bodies.push(BodyPreTypeckDesugarPlan { + body, + types: collector.types, + transforms: collector.transforms, + }); + } + bodies.extend(collector.nested_bodies); + bodies +} + +fn bool_source_value(text: &str) -> Option { + match text { + "true" => Some(true), + "false" => Some(false), + _ => None, + } +} + +struct ModuleCollector<'db> { + db: &'db dyn Db, + types: Vec>, + bodies: Vec>, + field_inits: Vec>, +} + +impl<'db> ModuleCollector<'db> { + fn item(&mut self, item: Item<'db>) { + match item { + Item::FunctionDef(function) => self.function(function), + Item::TypeAlias(alias) => self.type_alias(alias), + Item::AdtDef(adt) => self.adt(adt), + Item::ClassDef(class) => self.class(class), + Item::InstanceDef(instance) => self.instance(instance), + Item::ContractDef(contract) => self.contract(contract), + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } + } + + fn contract_item(&mut self, item: ContractItem<'db>) { + match item { + ContractItem::FunctionDef(function) => self.function(function), + ContractItem::TypeAlias(alias) => self.type_alias(alias), + ContractItem::AdtDef(adt) => self.adt(adt), + ContractItem::Error { .. } => {} + } + } + + fn function(&mut self, function: FunctionDef<'db>) { + self.func_sig(function.sig(self.db)); + if let Some(body) = function.body(self.db) { + self.bodies + .extend(pre_typeck_desugar_body_tree(self.db, body)); + } + } + + fn type_alias(&mut self, alias: TypeAlias<'db>) { + self.type_ref(alias.ty(self.db)); + } + + fn adt(&mut self, adt: AdtDef<'db>) { + for ctor in adt.ctors(self.db) { + self.type_ref(*ctor.fields.atom()); + } + } + + fn class(&mut self, class: ClassDef<'db>) { + for pred in class.super_preds(self.db) { + self.pred_ref(*pred); + } + self.pred_ref(class.head(self.db)); + for method in class.methods(self.db) { + self.func_sig(method); + } + } + + fn instance(&mut self, instance: InstanceDef<'db>) { + for pred in instance.preds(self.db) { + self.pred_ref(*pred); + } + self.pred_ref(instance.head(self.db)); + for method in instance.methods(self.db) { + self.function(*method); + } + } + + fn contract(&mut self, contract: ContractDef<'db>) { + for field in contract.fields(self.db) { + self.field(contract.def_id_value(self.db), field); + } + for item in contract.items(self.db) { + self.contract_item(*item); + } + } + + fn field(&mut self, contract: DefId<'db>, field: &FieldDef<'db>) { + self.type_ref(field.ty()); + if let Some(init) = field.init() { + self.field_init(contract, field.name().atom().text(self.db).to_owned(), init); + } + } + + fn field_init(&mut self, contract: DefId<'db>, field_name: String, init: &FieldInit<'db>) { + let mut collector = FieldInitCollector { + db: self.db, + exprs: &init.exprs, + transforms: Vec::new(), + }; + collector.expr(init.root); + if !collector.transforms.is_empty() { + self.field_inits.push(FieldInitPreTypeckDesugarPlan { + contract, + field_name, + transforms: collector.transforms, + }); + } + } + + fn func_sig(&mut self, sig: &FuncSig<'db>) { + for pred in &sig.preds { + self.pred_ref(*pred); + } + for param in sig.params.atom() { + self.func_param(param); + } + if let Some(ret) = sig.ret { + self.type_ref(ret); + } + } + + fn func_param(&mut self, param: &FuncParam<'db>) { + if let FuncParam::Typed { ty, .. } = param { + self.type_ref(*ty); + } + } + + fn pred_ref(&mut self, pred: PredRef<'db>) { + let kind = pred.kind(self.db); + self.type_ref(kind.ty); + for arg in kind.args.atom() { + self.type_ref(*arg); + } + } + + fn type_ref(&mut self, ty: TypeRef<'db>) { + match ty.kind(self.db) { + TypeRefKind::Named { args, .. } => { + for arg in args.atom() { + self.type_ref(*arg); + } + } + TypeRefKind::FixedArray { element, .. } => self.type_ref(*element), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + self.type_ref(*param); + } + self.type_ref(*ret); + } + TypeRefKind::Comptime { inner, .. } => self.type_ref(*inner), + TypeRefKind::Tuple { elems } => { + self.types.push(TypeProductDesugar { + ty, + origin: SourceOrigin::new(ty.span(self.db), SourceOriginKind::TupleType), + product: ProductShape::from_slice(elems.atom()), + }); + for elem in elems.atom() { + self.type_ref(*elem); + } + } + TypeRefKind::Error { .. } => {} + } + } +} + +struct BodyCollector<'db> { + db: &'db dyn Db, + body: FuncBody<'db>, + types: Vec>, + nested_bodies: Vec>, + transforms: Vec>, +} + +impl<'db> BodyCollector<'db> { + fn stmt(&mut self, stmt_id: Id>) { + match &self.body.stmts(self.db).get(stmt_id).kind { + StmtKind::Let { ty, init, .. } => { + if let Some(ty) = ty { + self.type_ref(*ty); + } + if let Some(init) = init { + self.expr(*init); + } + } + StmtKind::Return(expr) => { + if let Some(expr) = expr { + self.expr(*expr); + } + } + StmtKind::Expr(expr) => self.expr(*expr), + StmtKind::Assign { lhs, rhs, .. } => { + self.expr(*lhs); + self.expr(*rhs); + } + StmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + self.expr(*scrutinee); + } + for arm in arms { + for pat in &arm.pats { + self.pat(*pat); + } + for stmt in &arm.body { + self.stmt(*stmt); + } + } + } + StmtKind::For { + init, + cond, + post, + body, + } => { + for stmt in init { + self.stmt(*stmt); + } + self.expr(*cond); + for stmt in post { + self.stmt(*stmt); + } + for stmt in body { + self.stmt(*stmt); + } + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + let stmt = self.body.stmts(self.db).get(stmt_id); + self.transforms.push(PreTypeckTransform::IfStmtToMatch { + stmt: stmt_id, + origin: SourceOrigin::new(stmt.span, SourceOriginKind::IfStatement), + cond: *cond, + then_body: then_body.clone(), + else_body: else_body.clone(), + }); + self.expr(*cond); + for stmt in then_body { + self.stmt(*stmt); + } + if let Some(else_body) = else_body { + for stmt in else_body { + self.stmt(*stmt); + } + } + } + StmtKind::Block { body } => { + for stmt in body { + self.stmt(*stmt); + } + } + StmtKind::Assembly { .. } | StmtKind::Break | StmtKind::Continue | StmtKind::Error => {} + } + } + + fn expr(&mut self, expr_id: Id>) { + let expr = self.body.exprs(self.db).get(expr_id); + match &expr.kind { + ExprKind::Ident(name) => { + if let Some(value) = bool_source_value((*name.atom()).text(self.db)) { + self.transforms.push(PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Expr(expr_id), + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + value, + }); + } + } + ExprKind::DotCtor { name, args, .. } => { + if let Some(value) = bool_source_value((*name.atom()).text(self.db)) { + self.transforms.push(PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Expr(expr_id), + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + value, + }); + } + for arg in args { + self.expr(*arg); + } + } + ExprKind::Lambda { params, ret, body } => { + for param in params.atom() { + self.func_param(param); + } + if let Some(ret) = ret { + self.type_ref(*ret); + } + self.nested_bodies + .extend(pre_typeck_desugar_body_tree(self.db, *body)); + } + ExprKind::BinOp { lhs, rhs, .. } => { + self.expr(*lhs); + self.expr(*rhs); + } + ExprKind::Index { base, index } => { + self.expr(*base); + self.expr(*index); + } + ExprKind::Call { callee, args } => { + self.expr(*callee); + for arg in args { + self.expr(*arg); + } + } + ExprKind::Field { base, .. } => self.expr(*base), + ExprKind::Conversion { expr, ty } | ExprKind::TypeAscription { expr, ty } => { + self.expr(*expr); + self.type_ref(*ty); + } + ExprKind::UnaryOp { expr, .. } => self.expr(*expr), + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + self.transforms.push(PreTypeckTransform::IfExprToMatch { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::IfExpression), + cond: *cond, + then_expr: *then_expr, + else_expr: *else_expr, + }); + self.expr(*cond); + self.expr(*then_expr); + self.expr(*else_expr); + } + ExprKind::Tuple(elems) => { + self.transforms + .push(PreTypeckTransform::TupleExprToProduct { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::TupleExpr), + product: ProductShape::from_slice(elems), + }); + for elem in elems { + self.expr(*elem); + } + } + ExprKind::Proxy { ty, .. } => self.type_ref(*ty), + ExprKind::Lit(_) | ExprKind::Error => {} + } + } + + fn pat(&mut self, pat_id: Id>) { + let pat = self.body.pats(self.db).get(pat_id); + match &pat.kind { + PatKind::Ctor { head, args } => { + if let Some(value) = bool_source_value((*head.name().atom()).text(self.db)) { + self.transforms.push(PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Pat(pat_id), + origin: SourceOrigin::new(pat.span, SourceOriginKind::BoolConstructor), + value, + }); + } + for arg in args { + self.pat(*arg); + } + } + PatKind::ComptimeLabel { expr, .. } => self.expr(*expr), + PatKind::Tuple { elems } => { + self.transforms.push(PreTypeckTransform::TuplePatToProduct { + pat: pat_id, + origin: SourceOrigin::new(pat.span, SourceOriginKind::TuplePat), + product: ProductShape::from_slice(elems), + }); + for elem in elems { + self.pat(*elem); + } + } + PatKind::Var(name) => { + if let Some(value) = bool_source_value((*name.atom()).text(self.db)) { + self.transforms.push(PreTypeckTransform::BoolToUnitSum { + node: BoolUnitSumNode::Pat(pat_id), + origin: SourceOrigin::new(pat.span, SourceOriginKind::BoolConstructor), + value, + }); + } + } + PatKind::Wildcard | PatKind::Lit(_) | PatKind::Error => {} + } + } + + fn func_param(&mut self, param: &FuncParam<'db>) { + if let FuncParam::Typed { ty, .. } = param { + self.type_ref(*ty); + } + } + + fn type_ref(&mut self, ty: TypeRef<'db>) { + match ty.kind(self.db) { + TypeRefKind::Named { args, .. } => { + for arg in args.atom() { + self.type_ref(*arg); + } + } + TypeRefKind::FixedArray { element, .. } => self.type_ref(*element), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + self.type_ref(*param); + } + self.type_ref(*ret); + } + TypeRefKind::Comptime { inner, .. } => self.type_ref(*inner), + TypeRefKind::Tuple { elems } => { + self.types.push(TypeProductDesugar { + ty, + origin: SourceOrigin::new(ty.span(self.db), SourceOriginKind::TupleType), + product: ProductShape::from_slice(elems.atom()), + }); + for elem in elems.atom() { + self.type_ref(*elem); + } + } + TypeRefKind::Error { .. } => {} + } + } +} + +struct FieldInitCollector<'a, 'db> { + db: &'db dyn Db, + exprs: &'a Arena>, + transforms: Vec>, +} + +impl<'a, 'db> FieldInitCollector<'a, 'db> { + fn expr(&mut self, expr_id: Id>) { + let expr = self.exprs.get(expr_id); + match &expr.kind { + ExprKind::Ident(name) => { + if let Some(value) = bool_source_value((*name.atom()).text(self.db)) { + self.transforms + .push(FieldInitPreTypeckTransform::BoolToUnitSum { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + value, + }); + } + } + ExprKind::DotCtor { name, args, .. } => { + if let Some(value) = bool_source_value((*name.atom()).text(self.db)) { + self.transforms + .push(FieldInitPreTypeckTransform::BoolToUnitSum { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::BoolConstructor), + value, + }); + } + for arg in args { + self.expr(*arg); + } + } + ExprKind::BinOp { lhs, rhs, .. } => { + self.expr(*lhs); + self.expr(*rhs); + } + ExprKind::Index { base, index } => { + self.expr(*base); + self.expr(*index); + } + ExprKind::Call { callee, args } => { + self.expr(*callee); + for arg in args { + self.expr(*arg); + } + } + ExprKind::Field { base, .. } => self.expr(*base), + ExprKind::Conversion { expr, .. } + | ExprKind::TypeAscription { expr, .. } + | ExprKind::UnaryOp { expr, .. } => self.expr(*expr), + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + self.transforms + .push(FieldInitPreTypeckTransform::IfExprToMatch { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::IfExpression), + cond: *cond, + then_expr: *then_expr, + else_expr: *else_expr, + }); + self.expr(*cond); + self.expr(*then_expr); + self.expr(*else_expr); + } + ExprKind::Tuple(elems) => { + self.transforms + .push(FieldInitPreTypeckTransform::TupleExprToProduct { + expr: expr_id, + origin: SourceOrigin::new(expr.span, SourceOriginKind::TupleExpr), + product: ProductShape::from_slice(elems), + }); + for elem in elems { + self.expr(*elem); + } + } + ExprKind::Lambda { .. } + | ExprKind::Lit(_) + | ExprKind::Proxy { .. } + | ExprKind::Error => {} + } + } +} diff --git a/crates/hir-ty/src/display.rs b/crates/hir-ty/src/display.rs new file mode 100644 index 00000000..09da123d --- /dev/null +++ b/crates/hir-ty/src/display.rs @@ -0,0 +1,258 @@ +use hir::{ + Db as HirDb, + ast::ty::{TypeRef, TypeRefKind}, + nameres::ident_text, +}; + +use crate::{ClassId, Db, Pred, PredKind, Ty, TyCtor, TyKind}; + +pub(crate) fn display_var_name(index: u32, names: &[String]) -> String { + names + .get(index as usize) + .cloned() + .unwrap_or_else(|| "_".to_owned()) +} + +pub(crate) fn display_ty_source<'db>(db: &'db dyn Db, ty: Ty<'db>, names: &[String]) -> String { + match ty.kind(db) { + TyKind::Error => "".to_owned(), + TyKind::Unknown => "_".to_owned(), + TyKind::BoundVar(var) => display_var_name(var.index, names), + TyKind::Named { ctor, args } => { + if let TyCtor::Builtin(crate::BuiltinTyCtor::FixedArray(length)) = ctor + && let [element] = args.as_slice() + { + return format!("{}[{length}]", display_ty_source(db, *element, names)); + } + let name = display_ty_ctor_source(db, *ctor); + if args.is_empty() { + name + } else if name == "DynArray" && args.len() == 1 { + format!("{}[]", display_ty_source(db, args[0], names)) + } else if matches!(name.as_str(), "memory" | "storage" | "calldata") && args.len() == 1 + { + format!("{} {name}", display_ty_source(db, args[0], names)) + } else if name == "mapping" && args.len() == 2 { + format!( + "mapping({} => {})", + display_ty_source(db, args[0], names), + display_ty_source(db, args[1], names) + ) + } else { + format!( + "{name}<{}>", + args.iter() + .map(|arg| display_ty_source(db, *arg, names)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Function { params, ret } => { + let params = params + .iter() + .map(|param| display_ty_source(db, *param, names)) + .collect::>() + .join(", "); + format!( + "function({params}){}", + display_ty_return_suffix(db, *ret, names) + ) + } + TyKind::Tuple(elems) => { + if elems.is_empty() { + "()".to_owned() + } else { + format!( + "({})", + elems + .iter() + .map(|elem| display_ty_source(db, *elem, names)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Comptime(inner) => format!("comptime {}", display_ty_source(db, *inner, names)), + } +} + +pub(crate) fn display_ty_return_suffix<'db>( + db: &'db dyn Db, + ret: Ty<'db>, + names: &[String], +) -> String { + match ret.kind(db) { + TyKind::Tuple(elems) if elems.is_empty() => String::new(), + TyKind::Tuple(elems) => format!( + " returns ({})", + elems + .iter() + .map(|elem| display_ty_source(db, *elem, names)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", display_ty_source(db, ret, names)), + } +} + +fn display_ty_ctor_source<'db>(db: &'db dyn Db, ctor: TyCtor<'db>) -> String { + match ctor { + TyCtor::Builtin(ctor) => ctor.name().to_owned(), + TyCtor::User(user) => user + .def + .name(db) + .unwrap_or_else(|| format!("{:?}", user.def.kind(db))), + } +} + +pub(crate) fn display_class_source<'db>(db: &'db dyn Db, class: ClassId<'db>) -> String { + match class { + ClassId::Builtin(class) => class.name().to_owned(), + ClassId::User(def) => def + .name(db) + .unwrap_or_else(|| format!("{:?}", def.kind(db))), + } +} + +pub(crate) fn display_pred_source<'db>( + db: &'db dyn Db, + pred: Pred<'db>, + names: &[String], +) -> String { + match pred.kind(db) { + PredKind::InClass { class, main, args } => { + let main = display_ty_source(db, *main, names); + let class = display_class_source(db, *class); + if args.is_empty() { + format!("{main}: {class}") + } else { + let args = args + .iter() + .map(|arg| display_ty_source(db, *arg, names)) + .collect::>() + .join(", "); + format!("{main}: {class}<{args}>") + } + } + PredKind::Eq { lhs, rhs } => format!( + "{} ~ {}", + display_ty_source(db, *lhs, names), + display_ty_source(db, *rhs, names) + ), + PredKind::Error => "".to_owned(), + } +} + +/// Renders a source type reference using canonical new-syntax spelling. +/// +/// Unlike semantic [`Ty`] display, this preserves source-only function-type +/// qualifiers that are intentionally erased during type lowering. +pub fn display_type_ref_source<'db>(db: &'db dyn HirDb, ty: TypeRef<'db>) -> String { + match ty.kind(db) { + TypeRefKind::Named { + qualifier, + name, + args, + } => { + let mut out = String::new(); + let is_qualified = qualifier.is_some(); + if let Some(qualifier) = qualifier { + out.push_str(&ident_text(db, qualifier)); + out.push('.'); + } + let name = ident_text(db, name); + let args = args.atom(); + if !is_qualified && name == "DynArray" && args.len() == 1 { + return format!("{}[]", display_type_ref_source(db, args[0])); + } + if !is_qualified + && matches!(name.as_str(), "memory" | "storage" | "calldata") + && args.len() == 1 + { + return format!("{} {name}", display_type_ref_source(db, args[0])); + } + if !is_qualified && name == "mapping" && args.len() == 2 { + return format!( + "mapping({} => {})", + display_type_ref_source(db, args[0]), + display_type_ref_source(db, args[1]) + ); + } + out.push_str(&name); + if !args.is_empty() { + out.push('<'); + out.push_str( + &args + .iter() + .map(|arg| display_type_ref_source(db, *arg)) + .collect::>() + .join(", "), + ); + out.push('>'); + } + out + } + TypeRefKind::FixedArray { + element, length, .. + } => format!("{}[{length}]", display_type_ref_source(db, *element)), + TypeRefKind::Fn { + params, + visibility, + mutability, + ret, + .. + } => { + let mut out = format!( + "function({})", + params + .atom() + .iter() + .map(|param| display_type_ref_source(db, *param)) + .collect::>() + .join(", ") + ); + if let Some(visibility) = visibility { + out.push(' '); + out.push_str(visibility.atom().keyword()); + } + if let Some(mutability) = mutability { + out.push(' '); + out.push_str(mutability.atom().keyword()); + } + out.push_str(&display_type_ref_return_suffix(db, *ret)); + out + } + TypeRefKind::Comptime { inner, .. } => { + format!("comptime {}", display_type_ref_source(db, *inner)) + } + TypeRefKind::Tuple { elems } => { + format!( + "({})", + elems + .atom() + .iter() + .map(|elem| display_type_ref_source(db, *elem)) + .collect::>() + .join(", ") + ) + } + TypeRefKind::Error { .. } => "".to_owned(), + } +} + +pub(crate) fn display_type_ref_return_suffix<'db>(db: &'db dyn HirDb, ret: TypeRef<'db>) -> String { + match ret.kind(db) { + TypeRefKind::Tuple { elems } if elems.atom().is_empty() => String::new(), + TypeRefKind::Tuple { elems } => format!( + " returns ({})", + elems + .atom() + .iter() + .map(|elem| display_type_ref_source(db, *elem)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", display_type_ref_source(db, ret)), + } +} diff --git a/crates/hir-ty/src/infer/comptime.rs b/crates/hir-ty/src/infer/comptime.rs new file mode 100644 index 00000000..35397a7f --- /dev/null +++ b/crates/hir-ty/src/infer/comptime.rs @@ -0,0 +1,1406 @@ +use super::*; + +pub(super) struct TypeckDiagnosticCollector<'db> { + pub(super) db: &'db dyn Db, + pub(super) module: ModuleId<'db>, + pub(super) hir_module: Module<'db>, + pub(super) env: nameres::ModuleEnv<'db>, + pub(super) item_resolutions: hir_nameres::ItemResolutionMap<'db>, + pub(super) trait_env: TraitEnvId<'db>, + pub(super) diagnostics: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LatentComptimeParam { + index: usize, + function: String, + param: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SignatureRequirement { + TopLevel, + Method, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ComptimeValue { + Comptime, + Runtime, + Deferred, +} + +impl ComptimeValue { + fn from_all(values: impl IntoIterator) -> Self { + let mut saw_deferred = false; + for value in values { + match value { + ComptimeValue::Runtime => return ComptimeValue::Runtime, + ComptimeValue::Deferred => saw_deferred = true, + ComptimeValue::Comptime => {} + } + } + if saw_deferred { + ComptimeValue::Deferred + } else { + ComptimeValue::Comptime + } + } + + fn from_any_runtime(values: &[Self]) -> Self { + if values.contains(&ComptimeValue::Runtime) { + ComptimeValue::Runtime + } else if values.contains(&ComptimeValue::Deferred) { + ComptimeValue::Deferred + } else { + ComptimeValue::Comptime + } + } + + fn is_runtime(self) -> bool { + matches!(self, ComptimeValue::Runtime) + } +} + +#[derive(Debug, Clone)] +struct ComptimeParamInfo { + name: String, + is_comptime: bool, + has_type_var: bool, +} + +#[derive(Debug, Clone)] +struct ComptimeCallableSig { + name: String, + params: Vec, + ret_comptime: bool, +} + +struct ComptimeCheckResult<'db> { + diagnostics: Vec, + obligations: Vec>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ComptimeBindingKey<'db> { + Param(hir_nameres::ParamId<'db>), + Let { + body: FuncBody<'db>, + stmt: Id>, + }, + Pattern { + body: FuncBody<'db>, + pat: Id>, + }, +} + +struct ComptimeChecker<'db> { + db: &'db dyn Db, + entry_module: ModuleId<'db>, + hir_module: Module<'db>, + expr_resolutions: FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + pre_typeck_desugar: Vec>, + scopes: Vec>>, + bindings: FxHashMap, ComptimeValue>, + diagnostics: Vec, + obligations: Vec>, + current_function: String, + current_return_comptime: bool, +} + +impl<'db> ComptimeChecker<'db> { + fn new( + db: &'db dyn Db, + entry_module: ModuleId<'db>, + hir_module: Module<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + pre_typeck_desugar: Vec>, + function: FunctionDef<'db>, + ) -> Self { + let sig = function.sig(db); + let expr_resolutions = body_map + .exprs + .iter() + .map(|entry| ((entry.body, entry.expr), entry.resolution.clone())) + .collect(); + Self { + db, + entry_module, + hir_module, + expr_resolutions, + pre_typeck_desugar, + scopes: vec![FxHashMap::default()], + bindings: FxHashMap::default(), + diagnostics: Vec::new(), + obligations: Vec::new(), + current_function: ident_text(db, &sig.name), + current_return_comptime: type_ref_is_comptime(db, sig.ret.as_ref()), + } + } + + fn diagnostic_sources(&self) -> DiagnosticSourceMap<'_, 'db> { + DiagnosticSourceMap::new(self.db, &self.pre_typeck_desugar) + } + + fn desugar_view(&self) -> BodyDesugarView<'_, 'db> { + BodyDesugarView::new(&self.pre_typeck_desugar) + } + + fn stmt_label_span(&self, body: FuncBody<'db>, stmt: Id>) -> LabelSpan { + self.diagnostic_sources().stmt_label_span(body, stmt) + } + + fn expr_label_span(&self, body: FuncBody<'db>, expr: Id>) -> LabelSpan { + self.diagnostic_sources().expr_label_span(body, expr) + } + + fn check_function( + mut self, + function: FunctionDef<'db>, + body: FuncBody<'db>, + ) -> ComptimeCheckResult<'db> { + self.bind_params(body, function.sig(self.db).params.atom()); + self.check_stmt_sequence(body, body.top_level_stmts(self.db)); + ComptimeCheckResult { + diagnostics: self.diagnostics, + obligations: self.obligations, + } + } + + fn bind_params(&mut self, body: FuncBody<'db>, params: &[FuncParam<'db>]) { + for (index, param) in params.iter().enumerate() { + let Some(name) = param_name(self.db, param).map(str::to_owned) else { + continue; + }; + let key = ComptimeBindingKey::Param(hir_nameres::ParamId { + body, + index: hir_nameres::ParamIndex::from_usize(index), + }); + let value = if param_is_comptime(self.db, param) || self.current_return_comptime { + ComptimeValue::Comptime + } else { + ComptimeValue::Runtime + }; + self.bindings.insert(key, value); + self.add_name(name, key); + } + } + + fn check_stmt_sequence( + &mut self, + body: FuncBody<'db>, + stmts: &[Id>], + ) -> ComptimeValue { + let mut last = ComptimeValue::Comptime; + for (index, stmt) in stmts.iter().enumerate() { + last = self.check_stmt(body, *stmt, index + 1 == stmts.len()); + } + last + } + + fn check_stmt( + &mut self, + body: FuncBody<'db>, + stmt_id: Id>, + is_tail: bool, + ) -> ComptimeValue { + match &body.stmts(self.db).get(stmt_id).kind { + StmtKind::Let { + comptime, + name, + ty, + init, + } => { + let declared_comptime = comptime.is_some() + || type_ref_is_comptime(self.db, ty.as_ref()) + || ty + .as_ref() + .is_some_and(|ty| type_ref_is_integer(self.db, *ty)); + let init_value = init + .map(|expr| self.classify_expr(body, expr)) + .unwrap_or(ComptimeValue::Deferred); + let name_text = ident_text(self.db, name); + if declared_comptime && let Some(expr) = init { + self.obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::LetInit { + stmt: stmt_id, + name: name_text.clone(), + }, + }); + } + if declared_comptime && init_value.is_runtime() { + self.diagnostics.push(TypeckDiagnostic::ComptimeLetRuntime { + span: init + .map(|expr| self.expr_label_span(body, expr)) + .unwrap_or_else(|| self.stmt_label_span(body, stmt_id)), + name: name_text.clone(), + }); + } + let value = if declared_comptime && !init_value.is_runtime() { + ComptimeValue::Comptime + } else { + init_value + }; + let key = ComptimeBindingKey::Let { + body, + stmt: stmt_id, + }; + self.bindings.insert(key, value); + self.add_name(name_text, key); + ComptimeValue::Comptime + } + StmtKind::Return(expr) => { + let value = expr + .map(|expr| self.classify_expr(body, expr)) + .unwrap_or(ComptimeValue::Comptime); + if self.current_return_comptime + && let Some(expr) = expr + { + self.obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::Return { + context: self.current_function.clone(), + }, + }); + } + let span = expr + .map(|expr| self.expr_label_span(body, expr)) + .unwrap_or_else(|| self.stmt_label_span(body, stmt_id)); + self.check_comptime_return(span, value); + value + } + StmtKind::Expr(expr) => { + let value = self.classify_expr(body, *expr); + if is_tail { + if self.current_return_comptime { + self.obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::Return { + context: self.current_function.clone(), + }, + }); + } + self.check_comptime_return(self.expr_label_span(body, *expr), value); + } + value + } + StmtKind::Assign { lhs, rhs, .. } => { + let rhs_value = self.classify_expr(body, *rhs); + if let Some(key) = self.binding_key_for_expr(body, *lhs) { + self.bindings.insert(key, rhs_value); + } + rhs_value + } + StmtKind::Match { scrutinees, arms } => { + let scrutinee_values = scrutinees + .iter() + .map(|expr| self.classify_expr(body, *expr)) + .collect::>(); + for arm in arms { + self.push_scope(); + for (pat, value) in arm.pats.iter().zip(scrutinee_values.iter().copied()) { + self.bind_pattern(body, *pat, value); + } + self.check_stmt_sequence(body, &arm.body); + self.pop_scope(); + } + ComptimeValue::from_any_runtime(&scrutinee_values) + } + StmtKind::For { + init, + cond, + post, + body: for_body, + } => { + self.push_scope(); + self.check_stmt_sequence(body, init); + let cond_value = self.classify_expr(body, *cond); + self.check_stmt_sequence(body, for_body); + self.check_stmt_sequence(body, post); + self.pop_scope(); + cond_value + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + let input = if_stmt_match_input( + self.desugar_view(), + body, + stmt_id, + *cond, + then_body, + else_body.as_deref(), + ); + let cond_value = self.classify_expr(body, input.cond); + self.push_scope(); + let then_value = self.check_stmt_sequence(body, &input.then_body); + self.pop_scope(); + let else_value = if let Some(else_body) = input.else_body { + self.push_scope(); + let value = self.check_stmt_sequence(body, &else_body); + self.pop_scope(); + value + } else { + ComptimeValue::Comptime + }; + ComptimeValue::from_any_runtime(&[cond_value, then_value, else_value]) + } + StmtKind::Block { body: block } => { + self.push_scope(); + let value = self.check_stmt_sequence(body, block); + self.pop_scope(); + value + } + StmtKind::Assembly { .. } => ComptimeValue::Deferred, + StmtKind::Break | StmtKind::Continue => ComptimeValue::Deferred, + StmtKind::Error => ComptimeValue::Deferred, + } + } + + fn classify_expr(&mut self, body: FuncBody<'db>, expr_id: Id>) -> ComptimeValue { + match &body.exprs(self.db).get(expr_id).kind { + ExprKind::Lit(_) | ExprKind::Proxy { .. } => ComptimeValue::Comptime, + ExprKind::Ident(name) => self + .expr_resolution(body, expr_id) + .and_then(|resolution| self.value_for_resolution(resolution)) + .unwrap_or_else(|| self.lookup_name((*name.atom()).text(self.db))), + ExprKind::DotCtor { args, .. } | ExprKind::Tuple(args) => { + ComptimeValue::from_all(args.iter().map(|arg| self.classify_expr(body, *arg))) + } + ExprKind::Lambda { + params, + ret, + body: lambda_body, + } => { + self.check_lambda(*lambda_body, params.atom(), *ret); + ComptimeValue::Comptime + } + ExprKind::BinOp { lhs, rhs, .. } => ComptimeValue::from_all([ + self.classify_expr(body, *lhs), + self.classify_expr(body, *rhs), + ]), + ExprKind::Index { base, index } => ComptimeValue::from_all([ + self.classify_expr(body, *base), + self.classify_expr(body, *index), + ]), + ExprKind::Call { callee, args } => self.classify_call(body, expr_id, *callee, args), + ExprKind::Field { base, .. } => { + if self.expr_resolution(body, expr_id).is_some() { + ComptimeValue::Deferred + } else { + self.classify_expr(body, *base) + } + } + ExprKind::Conversion { expr, .. } | ExprKind::TypeAscription { expr, .. } => { + self.classify_expr(body, *expr) + } + ExprKind::UnaryOp { expr, .. } => self.classify_expr(body, *expr), + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + let input = if_expr_match_input( + self.desugar_view(), + body, + expr_id, + *cond, + *then_expr, + *else_expr, + ); + ComptimeValue::from_all([ + self.classify_expr(body, input.cond), + self.classify_expr(body, input.then_expr), + self.classify_expr(body, input.else_expr), + ]) + } + ExprKind::Error => ComptimeValue::Deferred, + } + } + + fn classify_call( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee: Id>, + args: &[Id>], + ) -> ComptimeValue { + let arg_values = args + .iter() + .map(|arg| self.classify_expr(body, *arg)) + .collect::>(); + let callee_resolution = self.expr_resolution(body, callee).cloned(); + if let Some(sig) = callee_resolution + .as_ref() + .and_then(|resolution| self.callable_sig_for_resolution(resolution)) + { + // Frontend C3 follows the reference CTDeferred model: do not inspect + // function or instance bodies here. Purity/runtime checks are carried + // by comptime obligations for selected-evidence specialization. + let skip_runtime_arg_diagnostics = sig + .params + .iter() + .any(|param| param.is_comptime && param.has_type_var); + for ((arg, arg_value), param) in args + .iter() + .zip(arg_values.iter().copied()) + .zip(sig.params.iter()) + { + if param.is_comptime { + self.obligations.push(ComptimeObligation { + body, + expr: *arg, + kind: ComptimeObligationKind::CallParam { + call_expr, + callee_expr: callee, + function: sig.name.clone(), + param: param.name.clone(), + }, + }); + } + if param.is_comptime && arg_value.is_runtime() && !skip_runtime_arg_diagnostics { + self.diagnostics + .push(TypeckDiagnostic::RuntimeToComptimeParam { + span: self.expr_label_span(body, *arg), + function: sig.name.clone(), + param: param.name.clone(), + }); + } + } + if sig.ret_comptime + && arg_values + .iter() + .all(|value| *value == ComptimeValue::Comptime) + { + ComptimeValue::Comptime + } else { + ComptimeValue::Deferred + } + } else { + ComptimeValue::Deferred + } + } + + fn check_lambda( + &mut self, + lambda_body: FuncBody<'db>, + params: &[FuncParam<'db>], + ret: Option>, + ) { + let previous_function = std::mem::replace(&mut self.current_function, "lambda".to_owned()); + let previous_return = std::mem::replace( + &mut self.current_return_comptime, + type_ref_is_comptime(self.db, ret.as_ref()), + ); + self.push_scope(); + self.bind_params(lambda_body, params); + self.check_stmt_sequence(lambda_body, lambda_body.top_level_stmts(self.db)); + self.pop_scope(); + self.current_function = previous_function; + self.current_return_comptime = previous_return; + } + + fn check_comptime_return(&mut self, span: LabelSpan, value: ComptimeValue) { + if self.current_return_comptime && value.is_runtime() { + self.diagnostics + .push(TypeckDiagnostic::ComptimeReturnRuntime { + span, + context: self.current_function.clone(), + }); + } + } + + fn bind_pattern(&mut self, body: FuncBody<'db>, pat: Id>, value: ComptimeValue) { + match &body.pats(self.db).get(pat).kind { + PatKind::Var(name) => { + let key = ComptimeBindingKey::Pattern { body, pat }; + self.bindings.insert(key, value); + self.add_name(ident_text(self.db, name), key); + } + PatKind::Ctor { args, .. } => { + for arg in args { + self.bind_pattern(body, *arg, value); + } + } + PatKind::Tuple { elems } => { + for elem in elems { + self.bind_pattern(body, *elem, value); + } + } + PatKind::ComptimeLabel { expr, .. } => { + self.classify_expr(body, *expr); + self.obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::PatternLabel { pat }, + }); + } + PatKind::Wildcard | PatKind::Lit(_) | PatKind::Error => {} + } + } + + fn binding_key_for_expr( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + match self.expr_resolution(body, expr)? { + hir_nameres::Resolution::Param(param) => Some(ComptimeBindingKey::Param(*param)), + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Let { body, stmt }) => { + Some(ComptimeBindingKey::Let { + body: *body, + stmt: *stmt, + }) + } + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Pattern { body, pat }) => { + Some(ComptimeBindingKey::Pattern { + body: *body, + pat: *pat, + }) + } + _ => None, + } + } + + fn value_for_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option { + let key = match resolution { + hir_nameres::Resolution::Param(param) => ComptimeBindingKey::Param(*param), + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Let { body, stmt }) => { + ComptimeBindingKey::Let { + body: *body, + stmt: *stmt, + } + } + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Pattern { body, pat }) => { + ComptimeBindingKey::Pattern { + body: *body, + pat: *pat, + } + } + _ => return None, + }; + Some( + self.bindings + .get(&key) + .copied() + .unwrap_or(ComptimeValue::Deferred), + ) + } + + fn callable_sig_for_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option { + match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => self.function_info(*def).map(|function| { + callable_sig_from_func_sig( + self.db, + function.function.sig(self.db), + &function.type_vars, + ) + }), + hir_nameres::Resolution::ClassMethod { class, name } => { + self.class_method_sig(*class, name) + } + hir_nameres::Resolution::Builtin(kind) => builtin_comptime_sig(*kind), + _ => None, + } + } + + fn function_info(&self, def: DefId<'db>) -> Option> { + let module = module_for_def(self.db, self.entry_module, def) + .and_then(|module| module_hir(self.db, module)) + .unwrap_or(self.hir_module); + find_function_info(self.db, module, def) + } + + fn class_method_sig(&self, class: DefId<'db>, name: &str) -> Option { + let module = module_for_def(self.db, self.entry_module, class) + .and_then(|module| module_hir(self.db, module)) + .unwrap_or(self.hir_module); + let class_info = find_class_info(self.db, module, class)?; + let method = class_info + .class + .methods(self.db) + .iter() + .find(|method| ident_text(self.db, &method.name) == name)?; + let type_vars = class_method_type_vars(self.db, class_info.class, method); + let mut sig = callable_sig_from_func_sig(self.db, method, &type_vars); + let class_name = class.name(self.db).unwrap_or_else(|| "trait".to_owned()); + sig.name = format!("{class_name}.{name}"); + Some(sig) + } + + fn expr_resolution( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option<&hir_nameres::Resolution<'db>> { + self.expr_resolutions.get(&(body, expr)) + } + + fn lookup_name(&self, name: &str) -> ComptimeValue { + self.lookup_key(name) + .and_then(|key| self.bindings.get(&key).copied()) + .unwrap_or(ComptimeValue::Deferred) + } + + fn lookup_key(&self, name: &str) -> Option> { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).copied()) + } + + fn add_name(&mut self, name: String, key: ComptimeBindingKey<'db>) { + if let Some(scope) = self.scopes.last_mut() { + scope.insert(name, key); + } + } + + fn push_scope(&mut self) { + self.scopes.push(FxHashMap::default()); + } + + fn pop_scope(&mut self) { + self.scopes.pop(); + } +} + +fn callable_sig_from_func_sig<'db>( + db: &'db dyn HirDb, + sig: &FuncSig<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> ComptimeCallableSig { + ComptimeCallableSig { + name: ident_text(db, &sig.name), + params: sig + .params + .atom() + .iter() + .enumerate() + .map(|(index, param)| ComptimeParamInfo { + name: param_name(db, param) + .map(str::to_owned) + .unwrap_or_else(|| format!("arg{index}")), + is_comptime: param_is_comptime(db, param), + has_type_var: param_mentions_type_var(db, param, type_vars), + }) + .collect(), + ret_comptime: type_ref_is_comptime(db, sig.ret.as_ref()), + } +} + +fn builtin_comptime_sig(kind: hir_nameres::BuiltinKind) -> Option { + use hir_nameres::{BuiltinClassMethod, BuiltinFunction, BuiltinKind}; + let sig = match kind { + BuiltinKind::Function(BuiltinFunction::WordToInteger) => ComptimeCallableSig { + name: "wordToInteger".to_owned(), + params: vec![ComptimeParamInfo { + name: "x".to_owned(), + is_comptime: false, + has_type_var: false, + }], + ret_comptime: true, + }, + BuiltinKind::Function(BuiltinFunction::WordFromInteger) => ComptimeCallableSig { + name: "wordFromInteger".to_owned(), + params: vec![ComptimeParamInfo { + name: "x".to_owned(), + is_comptime: false, + has_type_var: false, + }], + ret_comptime: true, + }, + BuiltinKind::Function( + BuiltinFunction::IntegerAdd + | BuiltinFunction::IntegerSub + | BuiltinFunction::IntegerMul + | BuiltinFunction::IntegerLt + | BuiltinFunction::IntegerEq, + ) => ComptimeCallableSig { + name: "integer primitive".to_owned(), + params: vec![ + ComptimeParamInfo { + name: "lhs".to_owned(), + is_comptime: false, + has_type_var: false, + }, + ComptimeParamInfo { + name: "rhs".to_owned(), + is_comptime: false, + has_type_var: false, + }, + ], + ret_comptime: true, + }, + BuiltinKind::ClassMethod(BuiltinClassMethod::IntFromInteger) => ComptimeCallableSig { + name: "Int.fromInteger".to_owned(), + params: vec![ComptimeParamInfo { + name: "x".to_owned(), + is_comptime: false, + has_type_var: false, + }], + ret_comptime: true, + }, + BuiltinKind::Function(BuiltinFunction::PrimAddWord | BuiltinFunction::PrimEqWord) + | BuiltinKind::Function(BuiltinFunction::Invoke) + | BuiltinKind::ClassMethod(BuiltinClassMethod::InvokableInvoke) + | BuiltinKind::Constructor(_) + | BuiltinKind::Type(_) + | BuiltinKind::Class(_) => return None, + }; + Some(sig) +} + +fn param_is_comptime<'db>(db: &'db dyn HirDb, param: &FuncParam<'db>) -> bool { + match param { + FuncParam::Typed { comptime, ty, .. } => { + comptime.is_some() || type_ref_is_comptime(db, Some(ty)) + } + FuncParam::Untyped { comptime, .. } => comptime.is_some(), + FuncParam::Error { .. } => false, + } +} + +fn param_mentions_type_var<'db>( + db: &'db dyn HirDb, + param: &FuncParam<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> bool { + match param { + FuncParam::Typed { ty, .. } => type_ref_mentions_type_var(db, *ty, type_vars), + FuncParam::Untyped { .. } | FuncParam::Error { .. } => false, + } +} + +fn type_ref_mentions_type_var<'db>( + db: &'db dyn HirDb, + ty: TypeRef<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> bool { + match ty.kind(db) { + TypeRefKind::Named { name, args, .. } => { + let text = (*name.atom()).text(db); + type_vars + .iter() + .any(|var| (*var.name.atom()).text(db) == text) + || args + .atom() + .iter() + .any(|arg| type_ref_mentions_type_var(db, *arg, type_vars)) + } + TypeRefKind::FixedArray { element, .. } => { + type_ref_mentions_type_var(db, *element, type_vars) + } + TypeRefKind::Fn { params, ret, .. } => { + params + .atom() + .iter() + .any(|param| type_ref_mentions_type_var(db, *param, type_vars)) + || type_ref_mentions_type_var(db, *ret, type_vars) + } + TypeRefKind::Comptime { inner, .. } => type_ref_mentions_type_var(db, *inner, type_vars), + TypeRefKind::Tuple { elems } => elems + .atom() + .iter() + .any(|elem| type_ref_mentions_type_var(db, *elem, type_vars)), + TypeRefKind::Error { .. } => false, + } +} + +pub(super) fn type_ref_is_comptime<'db>(db: &'db dyn HirDb, ty: Option<&TypeRef<'db>>) -> bool { + ty.is_some_and(|ty| matches!(ty.kind(db), TypeRefKind::Comptime { .. })) +} + +pub(super) fn type_ref_is_integer<'db>(db: &'db dyn HirDb, ty: TypeRef<'db>) -> bool { + match ty.kind(db) { + TypeRefKind::Comptime { inner, .. } => type_ref_is_integer(db, *inner), + TypeRefKind::Named { name, args, .. } => { + (*name.atom()).text(db) == "integer" && args.atom().is_empty() + } + _ => false, + } +} + +impl<'db> TypeckDiagnosticCollector<'db> { + pub(super) fn item( + &mut self, + item: Item<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + match item { + Item::FunctionDef(function) => { + self.function( + function, + enclosing_contract, + inherited_type_vars, + &[], + SignatureRequirement::TopLevel, + ); + } + Item::InstanceDef(instance) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + instance.def_id_value(self.db), + instance.type_var_elems(self.db), + )); + let instance_lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&inherited), + ); + let mut normalizer = + AliasNormalizer::new(self.db, self.hir_module, &self.item_resolutions); + let instance_givens = instance + .preds(self.db) + .iter() + .map(|pred| normalizer.normalize_pred(instance_lowerer.lower_pred(*pred))) + .collect::>(); + self.diagnostics.extend( + normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic) + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + self.extend_lowering_diagnostics(&instance_lowerer); + for method in instance.methods(self.db) { + self.function( + *method, + enclosing_contract, + &inherited, + &instance_givens, + SignatureRequirement::Method, + ); + } + } + Item::ClassDef(class) => { + self.class_signature_items(class, inherited_type_vars); + for method in class.methods(self.db) { + self.require_complete_method_signature(method); + } + } + Item::ContractDef(contract) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(self.db), + contract.ty_param_elems(self.db), + )); + self.contract_field_initializers(contract, &inherited); + for item in contract.items(self.db) { + match *item { + ContractItem::FunctionDef(function) => self.function( + function, + Some(contract.def_id_value(self.db)), + &inherited, + &[], + SignatureRequirement::TopLevel, + ), + ContractItem::TypeAlias(alias) => { + self.type_alias_signature(alias, &inherited); + } + ContractItem::AdtDef(adt) => { + self.adt_signature(adt, &inherited); + } + ContractItem::Error { .. } => {} + } + } + } + Item::TypeAlias(alias) => self.type_alias_signature(alias, inherited_type_vars), + Item::AdtDef(adt) => self.adt_signature(adt, inherited_type_vars), + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } + } + + fn type_alias_signature( + &mut self, + alias: TypeAlias<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + if alias.kind(self.db) == TypeAliasKind::ValueType + && let Err(error) = value_type_underlying_in_context( + self.db, + self.hir_module, + &self.item_resolutions, + alias.def_id_value(self.db), + ) + { + self.diagnostics.push(AnyDiagnostic::Typeck( + TypeckDiagnostic::InvalidValueTypeDefinition { + span: error.span, + name: error.name, + reason: error.reason, + } + .lower(), + )); + } + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(type_var_bindings( + alias.def_id_value(self.db), + alias.ty_param_elems(self.db), + )); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + lowerer.lower_type_alias(alias); + self.extend_lowering_diagnostics(&lowerer); + } + + fn adt_signature( + &mut self, + adt: AdtDef<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(type_var_bindings( + adt.def_id_value(self.db), + adt.ty_param_elems(self.db), + )); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + for ctor in adt.ctors(self.db) { + lowerer.lower_adt_ctor(adt, ctor); + } + self.extend_lowering_diagnostics(&lowerer); + } + + fn class_signature_items( + &mut self, + class: ClassDef<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(type_var_bindings( + class.def_id_value(self.db), + class.type_var_elems(self.db), + )); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + lowerer.lower_pred(class.head(self.db)); + for pred in class.super_preds(self.db) { + lowerer.lower_pred(*pred); + } + self.extend_lowering_diagnostics(&lowerer); + for method in class.methods(self.db) { + let mut method_type_vars = type_vars.clone(); + method_type_vars.extend(hir_nameres::type_var_bindings_from( + class.def_id_value(self.db), + class.type_var_elems(self.db).len() as u32, + &method.type_vars, + )); + let method_lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&method_type_vars), + ); + method_lowerer.lower_class_method(class, method); + self.extend_lowering_diagnostics(&method_lowerer); + } + } + + fn function( + &mut self, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + extra_givens: &[Pred<'db>], + signature_requirement: SignatureRequirement, + ) { + let sig = function.sig(self.db); + if matches!(function.kind(self.db), FuncKind::Function) { + let complete = match signature_requirement { + SignatureRequirement::TopLevel => self.require_complete_signature(sig), + SignatureRequirement::Method => self.require_complete_method_signature(sig), + }; + if !complete { + return; + } + } + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(sig_type_vars(function.def_id_value(self.db), sig)); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut lowered = lowerer.lower_function(function); + self.extend_lowering_diagnostics(&lowerer); + let mut normalizer = AliasNormalizer::new(self.db, self.hir_module, &self.item_resolutions); + lowered.scheme = normalizer.normalize_scheme(lowered.scheme); + lowered.params = lowered + .params + .into_iter() + .map(|param| normalizer.normalize_ty(param)) + .collect(); + lowered.ret = normalizer.normalize_ty(lowered.ret); + self.diagnostics.extend( + normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic) + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + let Some(body) = function.body(self.db) else { + return; + }; + let context = hir_nameres::BodyResolutionContext { + module: self.hir_module, + enclosing_contract, + params: param_bindings(sig.params.atom()), + type_vars: type_vars.clone(), + }; + let body_map = hir_nameres::resolve_body_with_imports_and_policy( + self.db, + body, + &context, + &self.env, + hir_nameres::NameresDiagnosticPolicy::Emit, + ); + if !body_map.diagnostics.is_empty() { + return; + } + let pre_typeck_desugar = crate::pre_typeck_desugar_body_tree(self.db, body); + let body_arity_diagnostics = body_type_constructor_arity_diagnostics( + self.db, + self.module, + body, + &body_map, + &pre_typeck_desugar, + ); + if !body_arity_diagnostics.is_empty() { + self.diagnostics.extend( + body_arity_diagnostics + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + return; + } + let ComptimeCheckResult { + diagnostics, + obligations: _obligations, + } = ComptimeChecker::new( + self.db, + self.module, + self.hir_module, + &body_map, + pre_typeck_desugar.clone(), + function, + ) + .check_function(function, body); + self.diagnostics.extend( + diagnostics + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + let mut givens = lowered.scheme.body(self.db).preds(self.db).clone(); + givens.extend(extra_givens.iter().copied()); + let trait_env = trait_env_with_givens(self.db, self.trait_env, givens); + let ctx = BodyTyContext::new( + self.hir_module, + body_map.clone(), + type_vars, + lowered.params, + Some(lowered.ret), + ) + .with_param_names(param_names(self.db, sig.params.atom())) + .with_ret_display( + sig.ret + .map(|ret| crate::display::display_type_ref_source(self.db, ret)), + ) + .with_entry_module(self.module) + .with_trait_env(trait_env) + .with_partial_data(partial_data_entries(&self.env)) + .with_pre_typeck_desugar(pre_typeck_desugar); + let result = infer_body(self.db, body, ctx); + self.latent_comptime_call_diagnostics(body, &body_map, &result); + self.diagnostics.extend( + result + .diagnostics + .iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + } + + fn latent_comptime_call_diagnostics( + &mut self, + body: FuncBody<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + result: &InferenceResult<'db>, + ) { + for (call_expr, expr) in body.exprs(self.db).iter() { + let ExprKind::Call { callee, args } = &expr.kind else { + continue; + }; + let Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + }) = body_expr_resolution(body_map, body, *callee) + else { + continue; + }; + let latent = self.latent_comptime_params(*def); + if latent.is_empty() { + continue; + } + for latent_param in latent { + let Some(arg) = args.get(latent_param.index).copied() else { + continue; + }; + let Some(arg_ty) = result.expr_ty(body, arg) else { + continue; + }; + if !ty_is_closed_concrete(self.db, arg_ty) + || ty_requires_comptime(self.db, arg_ty) + || expr_is_literal_comptime(self.db, body, arg) + { + continue; + } + self.diagnostics.push(AnyDiagnostic::Typeck( + TypeckDiagnostic::RuntimeToComptimeParam { + span: LabelSpan::from_span( + self.db, + body.exprs(self.db).get(arg).span(self.db), + ), + function: latent_param.function, + param: latent_param.param, + } + .lower(), + )); + let _ = call_expr; + } + } + } + + fn latent_comptime_params(&self, def: DefId<'db>) -> Vec { + let Some(info) = self.function_lookup(def) else { + return Vec::new(); + }; + let Some(body) = info.function.body(self.db) else { + return Vec::new(); + }; + let module = module_for_def(self.db, self.module, def) + .and_then(|module| module_hir(self.db, module)) + .unwrap_or(self.hir_module); + let Some(body_map) = + body_resolution_for_function_with_imports(self.db, module, &info, Some(&self.env)) + else { + return Vec::new(); + }; + if !body_map.diagnostics.is_empty() { + return Vec::new(); + } + let pre_typeck_desugar = crate::pre_typeck_desugar_body_tree(self.db, body); + let ComptimeCheckResult { + diagnostics: _, + obligations, + } = ComptimeChecker::new( + self.db, + self.module, + module, + &body_map, + pre_typeck_desugar, + info.function, + ) + .check_function(info.function, body); + let param_names = param_names(self.db, info.function.sig(self.db).params.atom()); + let mut out = Vec::new(); + for obligation in obligations { + let ComptimeObligationKind::CallParam { + function, param, .. + } = obligation.kind + else { + continue; + }; + let ExprKind::Ident(name) = &body.exprs(self.db).get(obligation.expr).kind else { + continue; + }; + let name = (*name.atom()).text(self.db); + let Some(index) = param_names.iter().position(|param| param == name) else { + continue; + }; + out.push(LatentComptimeParam { + index, + function, + param, + }); + } + out.sort_by_key(|param| param.index); + out.dedup(); + out + } + + fn function_lookup(&self, def: DefId<'db>) -> Option> { + let module = module_for_def(self.db, self.module, def) + .and_then(|module| module_hir(self.db, module)) + .unwrap_or(self.hir_module); + find_function_info(self.db, module, def) + } + + fn contract_field_initializers( + &mut self, + contract: ContractDef<'db>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + for (index, field) in contract.fields(self.db).iter().enumerate() { + if field.init().is_none() { + continue; + } + let field_lowerer = TypeLowering::from_item_resolutions( + self.db, + &self.item_resolutions, + BinderEnv::from_type_vars(inherited_type_vars), + ); + let field_ty = field_lowerer.lower_field(field).ty; + self.extend_lowering_diagnostics(&field_lowerer); + let mut normalizer = + AliasNormalizer::new(self.db, self.hir_module, &self.item_resolutions); + let field_ty = normalizer.normalize_ty(field_ty); + self.diagnostics.extend( + normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic) + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + + let body = self.field_initializer_body(contract, field, index as u32); + let context = hir_nameres::BodyResolutionContext { + module: self.hir_module, + enclosing_contract: Some(contract.def_id_value(self.db)), + params: Vec::new(), + type_vars: inherited_type_vars.to_vec(), + }; + let body_map = hir_nameres::resolve_body_with_imports_and_policy( + self.db, + body, + &context, + &self.env, + hir_nameres::NameresDiagnosticPolicy::Emit, + ); + if !body_map.diagnostics.is_empty() { + self.diagnostics.extend( + body_map + .diagnostics + .iter() + .cloned() + .map(AnyDiagnostic::Nameres), + ); + continue; + } + let pre_typeck_desugar = crate::pre_typeck_desugar_body_tree(self.db, body); + let ctx = BodyTyContext::new( + self.hir_module, + body_map, + inherited_type_vars.to_vec(), + Vec::new(), + Some(field_ty), + ) + .with_entry_module(self.module) + .with_ret_display(Some(crate::display::display_type_ref_source( + self.db, + field.ty(), + ))) + .with_trait_env(self.trait_env) + .with_partial_data(partial_data_entries(&self.env)) + .with_pre_typeck_desugar(pre_typeck_desugar); + self.diagnostics.extend( + body_ty_diagnostics(self.db, body, ctx) + .iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + } + } + + fn field_initializer_body( + &self, + contract: ContractDef<'db>, + field: &FieldDef<'db>, + index: u32, + ) -> FuncBody<'db> { + let init = field.init().expect("field initializer"); + let field_name = ident_text(self.db, field.name()); + let body_def = DefId::new( + self.db, + contract.def_id_value(self.db).file(self.db), + Some(contract.def_id_value(self.db)), + DefKind::FuncBody, + Some(format!("{field_name}$field_init")), + Some(index.to_string()), + Disambiguator::ZERO, + ); + let mut stmts = Arena::new(); + let stmt = stmts.alloc(Stmt { + span: init.span, + kind: StmtKind::Return(Some(init.root)), + }); + FuncBody::new( + self.db, + body_def, + init.span, + vec![stmt], + stmts, + init.exprs.clone(), + Arena::new(), + ) + } + + fn extend_lowering_diagnostics(&mut self, lowerer: &TypeLowering<'db>) { + self.diagnostics.extend( + lowerer + .take_diagnostics() + .into_iter() + .map(lowering_diagnostic_to_typeck) + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + } + + fn require_complete_signature(&mut self, sig: &FuncSig<'db>) -> bool { + if is_complete_signature(sig) { + return true; + } + self.diagnostics.push(AnyDiagnostic::Typeck( + TypeckDiagnostic::IncompleteSignature { + span: LabelSpan::from_span(self.db, sig.name.span(self.db)), + signature: format_func_sig(self.db, sig), + } + .lower(), + )); + false + } + + fn require_complete_method_signature(&mut self, sig: &FuncSig<'db>) -> bool { + if is_complete_signature(sig) { + return true; + } + self.diagnostics.push(AnyDiagnostic::Typeck( + TypeckDiagnostic::IncompleteMethodSignature { + span: LabelSpan::from_span(self.db, sig.name.span(self.db)), + signature: format_func_sig(self.db, sig), + } + .lower(), + )); + false + } +} diff --git a/crates/hir-ty/src/infer/coverage_adapter.rs b/crates/hir-ty/src/infer/coverage_adapter.rs new file mode 100644 index 00000000..3f0aace3 --- /dev/null +++ b/crates/hir-ty/src/infer/coverage_adapter.rs @@ -0,0 +1,777 @@ +use super::*; +use num_bigint::BigUint; + +impl<'db> InferCtx<'db> { + pub(super) fn ensure_visible_pattern_coverage( + &mut self, + body: FuncBody<'db>, + scrutinee_exprs: &[Id>], + scrutinees: &[InferTy<'db>], + arms: &[MatchArm<'db>], + ) { + for (index, scrutinee) in scrutinees.iter().enumerate() { + let Some(ty) = self.partial_data_scrutinee_name(scrutinee.clone()) else { + continue; + }; + if arms + .iter() + .any(|arm| self.arm_has_catch_all_at(body, arm, index)) + { + continue; + } + self.diagnostics + .push(TypeckDiagnostic::HiddenConstructorCoverage { + span: scrutinee_exprs + .get(index) + .map(|expr| self.expr_label_span(body, *expr)) + .unwrap_or_else(|| self.body_label_span(body)), + ty, + }); + } + } + + fn arm_has_catch_all_at(&self, body: FuncBody<'db>, arm: &MatchArm<'db>, index: usize) -> bool { + arm.pats.get(index).is_some_and(|pat| { + matches!( + body.pats(self.db).get(*pat).kind, + PatKind::Wildcard | PatKind::Var(_) + ) + }) + } + + fn partial_data_scrutinee_name(&mut self, ty: InferTy<'db>) -> Option { + let expanded = self.expand_infer_aliases(ty, &mut FxHashSet::default()); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } = self.engine.resolve(expanded) + else { + return None; + }; + let name = def.name(self.db)?; + self.partial_data + .iter() + .any(|(visible_name, _)| { + visible_name == &name + || visible_name + .rsplit('.') + .next() + .is_some_and(|leaf| leaf == name) + }) + .then_some(name) + } + + pub(super) fn ensure_match_coverage( + &mut self, + body: FuncBody<'db>, + scrutinee_exprs: &[Id>], + scrutinees: &[InferTy<'db>], + arms: &[MatchArm<'db>], + ) { + if arms.iter().any(|arm| arm.pats.len() != scrutinees.len()) { + return; + } + for (index, scrutinee) in scrutinees.iter().enumerate() { + if self + .partial_data_scrutinee_name(scrutinee.clone()) + .is_some() + && !arms + .iter() + .any(|arm| self.arm_has_catch_all_at(body, arm, index)) + { + return; + } + } + + let mut tys = Vec::with_capacity(scrutinees.len()); + for scrutinee in scrutinees { + let ty = self.coverage_ty(scrutinee.clone()); + if matches!(ty, InferTy::Error) { + return; + } + tys.push(ty); + } + + let mut coverage_arms = Vec::with_capacity(arms.len()); + for arm in arms { + let mut row = Vec::with_capacity(arm.pats.len()); + for (pat, ty) in arm.pats.iter().zip(tys.iter()) { + if self.pat_is_poisoned(body, *pat) { + return; + } + let Some(coverage_pat) = self.coverage_pat(body, *pat, ty.clone()) else { + return; + }; + row.push(coverage_pat); + } + coverage_arms.push(CoverageArm::new(row)); + } + + let Ok(analysis) = matchcov::analyze(self, &tys, &coverage_arms) else { + return; + }; + + for (arm_index, reachability) in analysis.arms.iter().enumerate() { + if matches!( + reachability, + Reachability::Unreachable(UnreachableReason::Pattern) + ) && let Some(arm) = arms.get(arm_index) + { + self.diagnostics + .push(TypeckDiagnostic::UnreachableMatchArm { + span: self.label_span(arm.span(self.db)), + }); + } + } + + if let Exhaustiveness::NonExhaustive(witness) = &analysis.exhaustiveness { + let span = scrutinee_exprs + .first() + .map(|expr| self.expr_label_span(body, *expr)) + .unwrap_or_else(|| self.body_label_span(body)); + self.diagnostics.push(TypeckDiagnostic::NonExhaustiveMatch { + span, + missing: self.display_witness_row(witness), + }); + } + } + + fn coverage_ty(&mut self, ty: InferTy<'db>) -> InferTy<'db> { + let ty = self.normalize_aliases(ty); + let ty = self.expand_infer_aliases(ty, &mut FxHashSet::default()); + match self.engine.resolve(ty) { + InferTy::Comptime(inner) => self.coverage_ty(*inner), + ty => ty, + } + } + + fn coverage_pat( + &mut self, + body: FuncBody<'db>, + pat_id: Id>, + expected: InferTy<'db>, + ) -> Option> { + if self.pat_is_poisoned(body, pat_id) { + return None; + } + let kind = body.pats(self.db).get(pat_id).kind.clone(); + match kind { + PatKind::Wildcard => Some(CoveragePat::Wildcard), + PatKind::Var(name) => { + let name = (*name.atom()).text(self.db).to_owned(); + self.coverage_ctor_for_pat(body, pat_id, &name, &[], expected) + .map(|(ctor, _)| CoveragePat::constant(CoverageHead::Ctor(ctor))) + .or(Some(CoveragePat::Wildcard)) + } + PatKind::Lit(LitKind::Error) => None, + PatKind::Lit(lit) => Some(self.coverage_lit_pat(&lit, expected)), + PatKind::ComptimeLabel { .. } => None, + PatKind::Tuple { elems } => { + let expected = self.coverage_ty(expected); + if let Some(field_tys) = self.product_field_tys(expected.clone()) + && field_tys.len() == elems.len() + { + return self.coverage_product_pat(body, &elems, &field_tys); + } + let field_tys = match expected { + InferTy::Tuple(field_tys) if field_tys.len() == elems.len() => field_tys, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + } if args.is_empty() && elems.is_empty() => Vec::new(), + _ => return None, + }; + let mut fields = Vec::with_capacity(elems.len()); + for (elem, field_ty) in elems.into_iter().zip(field_tys) { + fields.push(self.coverage_pat(body, elem, field_ty)?); + } + let ctor = if fields.is_empty() { + CoverageCtor::Builtin(BuiltinCoverageCtor::Unit) + } else { + CoverageCtor::Builtin(BuiltinCoverageCtor::Tuple(fields.len())) + }; + Some(CoveragePat::constructor(CoverageHead::Ctor(ctor), fields)) + } + PatKind::Ctor { head, args } => { + let name = (*head.name().atom()).text(self.db).to_owned(); + let (ctor, field_tys) = + self.coverage_ctor_for_pat(body, pat_id, &name, &args, expected)?; + if field_tys.len() != args.len() { + return None; + } + let mut fields = Vec::with_capacity(args.len()); + for (arg, field_ty) in args.into_iter().zip(field_tys) { + fields.push(self.coverage_pat(body, arg, field_ty)?); + } + Some(CoveragePat::constructor(CoverageHead::Ctor(ctor), fields)) + } + PatKind::Error => None, + } + } + + fn coverage_ctor_for_pat( + &mut self, + body: FuncBody<'db>, + pat_id: Id>, + name: &str, + args: &[Id>], + expected: InferTy<'db>, + ) -> Option<(CoverageCtor<'db>, Vec>)> { + let resolution = self + .pat_resolutions + .get(&(body, pat_id)) + .cloned() + .unwrap_or(hir_nameres::Resolution::Err); + let ctor = match resolution { + hir_nameres::Resolution::Ctor { ty, index } => self.user_ctor_head(ty, index)?, + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor(ctor)) => { + self.builtin_coverage_ctor_for_expected(ctor, expected.clone())? + } + hir_nameres::Resolution::DotCtorDeferred => { + self.coverage_ctor_by_name_for_expected(name, expected.clone())? + } + hir_nameres::Resolution::Err => return None, + _ if args.is_empty() => return None, + _ => return None, + }; + let field_tys = self.field_tys_for_ctor(&ctor, expected)?; + Some((ctor, field_tys)) + } + + fn finite_constructor_space(&mut self, ty: InferTy<'db>) -> Option>> { + match self.coverage_ty(ty) { + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Some(vec![ + CoverageCtor::Builtin(BuiltinCoverageCtor::False), + CoverageCtor::Builtin(BuiltinCoverageCtor::True), + ]), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Some(vec![CoverageCtor::Builtin(BuiltinCoverageCtor::Unit)]), + InferTy::Tuple(fields) if fields.is_empty() => { + Some(vec![CoverageCtor::Builtin(BuiltinCoverageCtor::Unit)]) + } + InferTy::Tuple(fields) => Some(vec![CoverageCtor::Builtin( + BuiltinCoverageCtor::Tuple(fields.len()), + )]), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => Some(vec![CoverageCtor::Builtin(BuiltinCoverageCtor::Pair)]), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Sum), + args, + } if args.len() == 2 => Some(vec![ + CoverageCtor::Builtin(BuiltinCoverageCtor::Inl), + CoverageCtor::Builtin(BuiltinCoverageCtor::Inr), + ]), + InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } => { + let ctors = self.user_ctor_heads(def); + (!ctors.is_empty()).then_some(ctors) + } + _ => None, + } + } + + fn coverage_ctor_by_name_for_expected( + &mut self, + name: &str, + expected: InferTy<'db>, + ) -> Option> { + match self.coverage_ty(expected.clone()) { + InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } => { + let info = self.adt_lookup(def)?; + let matches = info + .adt + .ctors(self.db) + .iter() + .enumerate() + .filter(|(_, ctor)| ident_text(self.db, &ctor.name) == name) + .map(|(index, _)| CoverageCtor::User { + ty: def, + index: hir_nameres::CtorIndex::from_usize(index), + }) + .collect::>(); + match matches.as_slice() { + [ctor] => Some(ctor.clone()), + _ => None, + } + } + _ => { + let kind = builtin_ctor_kind_by_name(name)?; + let hir_nameres::BuiltinKind::Constructor(ctor) = kind else { + return None; + }; + self.builtin_coverage_ctor_for_expected(ctor, expected) + } + } + } + + fn field_tys_for_ctor( + &mut self, + ctor: &CoverageCtor<'db>, + scrutinee: InferTy<'db>, + ) -> Option>> { + let scrutinee = self.coverage_ty(scrutinee); + match ctor { + CoverageCtor::Builtin(builtin) => self.builtin_field_tys(*builtin, scrutinee), + CoverageCtor::User { ty, index, .. } => { + let scheme = self.lookup_adt_ctor_scheme(*ty, *index)?; + let instantiated = self.engine.instantiate_scheme(scheme); + if !instantiated.obligations.is_empty() || !instantiated.equality_errors.is_empty() + { + return None; + } + match self.engine.resolve(instantiated.ty) { + InferTy::Function { params, ret } => { + self.engine.unify(*ret, scrutinee).ok()?; + Some( + params + .into_iter() + .map(|param| self.coverage_ty(param)) + .collect(), + ) + } + ty => { + self.engine.unify(ty, scrutinee).ok()?; + Some(Vec::new()) + } + } + } + } + } + + fn builtin_field_tys( + &mut self, + ctor: BuiltinCoverageCtor, + scrutinee: InferTy<'db>, + ) -> Option>> { + match (ctor, self.coverage_ty(scrutinee)) { + ( + BuiltinCoverageCtor::True | BuiltinCoverageCtor::False, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Bool), + args, + }, + ) if args.is_empty() => Some(Vec::new()), + ( + BuiltinCoverageCtor::Unit, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + ) if args.is_empty() => Some(Vec::new()), + (BuiltinCoverageCtor::Unit, InferTy::Tuple(fields)) if fields.is_empty() => { + Some(Vec::new()) + } + (BuiltinCoverageCtor::Tuple(len), InferTy::Tuple(fields)) if fields.len() == len => { + Some(fields) + } + (BuiltinCoverageCtor::Tuple(len), ty) => self + .product_field_tys(ty) + .filter(|fields| fields.len() == len), + ( + BuiltinCoverageCtor::Pair, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + args, + }, + ) if args.len() == 2 => Some(args), + (BuiltinCoverageCtor::Pair, InferTy::Tuple(fields)) if fields.len() == 2 => { + Some(fields) + } + ( + BuiltinCoverageCtor::Inl, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Sum), + args, + }, + ) if args.len() == 2 => Some(vec![args[0].clone()]), + ( + BuiltinCoverageCtor::Inr, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Sum), + args, + }, + ) if args.len() == 2 => Some(vec![args[1].clone()]), + _ => None, + } + } + + fn builtin_coverage_ctor(&self, ctor: hir_nameres::BuiltinCtor) -> CoverageCtor<'db> { + let ctor = match ctor { + hir_nameres::BuiltinCtor::True => BuiltinCoverageCtor::True, + hir_nameres::BuiltinCtor::False => BuiltinCoverageCtor::False, + hir_nameres::BuiltinCtor::Unit => BuiltinCoverageCtor::Unit, + hir_nameres::BuiltinCtor::Pair => BuiltinCoverageCtor::Pair, + hir_nameres::BuiltinCtor::Inl => BuiltinCoverageCtor::Inl, + hir_nameres::BuiltinCtor::Inr => BuiltinCoverageCtor::Inr, + }; + CoverageCtor::Builtin(ctor) + } + + fn builtin_coverage_ctor_for_expected( + &mut self, + ctor: hir_nameres::BuiltinCtor, + expected: InferTy<'db>, + ) -> Option> { + let canonical = match (ctor, self.coverage_ty(expected.clone())) { + (hir_nameres::BuiltinCtor::Pair, InferTy::Tuple(fields)) if fields.len() == 2 => { + CoverageCtor::Builtin(BuiltinCoverageCtor::Tuple(2)) + } + (hir_nameres::BuiltinCtor::Unit, InferTy::Tuple(fields)) if fields.is_empty() => { + CoverageCtor::Builtin(BuiltinCoverageCtor::Unit) + } + _ => self.builtin_coverage_ctor(ctor), + }; + self.field_tys_for_ctor(&canonical, expected) + .map(|_| canonical) + } + + fn user_ctor_heads(&self, ty: DefId<'db>) -> Vec> { + let Some(info) = self.adt_lookup(ty) else { + return Vec::new(); + }; + info.adt + .ctors(self.db) + .iter() + .enumerate() + .map(|(index, _)| CoverageCtor::User { + ty, + index: hir_nameres::CtorIndex::from_usize(index), + }) + .collect() + } + + fn user_ctor_head( + &self, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, + ) -> Option> { + self.user_ctor_heads(ty) + .into_iter() + .find(|ctor| matches!(ctor, CoverageCtor::User { index: ctor_index, .. } if *ctor_index == index)) + } + + fn adt_lookup(&self, def: DefId<'db>) -> Option> { + if let Some(info) = find_adt_info(self.db, self.module, def) { + return Some(info); + } + let entry = self.entry_module?; + let module = module_for_def(self.db, entry, def)?; + let hir_module = module_hir(self.db, module)?; + find_adt_info(self.db, hir_module, def) + } + + fn display_witness_row(&self, row: &[CoverageWitness>]) -> String { + row.iter() + .map(|pat| self.display_witness_pat(pat)) + .collect::>() + .join(", ") + } + + fn display_witness_pat(&self, pat: &CoverageWitness>) -> String { + match pat { + CoverageWitness::Wildcard | CoverageWitness::OtherThan { .. } => "_".to_owned(), + CoverageWitness::Constructor { head, arguments } => { + let fields = arguments + .iter() + .map(|field| self.display_witness_pat(field)) + .collect::>(); + match head { + CoverageHead::Literal(_) => "_".to_owned(), + CoverageHead::Ctor(CoverageCtor::User { ty, index }) => { + let name = self.display_user_ctor(*ty, *index); + self.display_ctor_pat(&name, &fields) + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::True)) => { + "true".to_owned() + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::False)) => { + "false".to_owned() + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Unit)) => { + "()".to_owned() + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Tuple(_))) => { + format!("({})", fields.join(", ")) + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Pair)) => { + self.display_ctor_pat("pair", &fields) + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Inl)) => { + self.display_ctor_pat("inl", &fields) + } + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Inr)) => { + self.display_ctor_pat("inr", &fields) + } + } + } + } + } + + fn display_user_ctor(&self, ty: DefId<'db>, index: hir_nameres::CtorIndex) -> String { + let ty_name = ty + .name(self.db) + .or_else(|| { + self.adt_lookup(ty) + .map(|info| ident_text(self.db, &info.adt.name_elem(self.db))) + }) + .unwrap_or_else(|| "adt".to_owned()); + let ctor_name = self + .adt_lookup(ty) + .and_then(|info| info.adt.ctors(self.db).get(index.as_usize()).cloned()) + .map(|ctor| ident_text(self.db, &ctor.name)) + .unwrap_or_else(|| format!("constructor{}", index.as_u32())); + format!("{ty_name}.{ctor_name}") + } + + fn display_ctor_pat(&self, name: &str, fields: &[String]) -> String { + if fields.is_empty() { + name.to_owned() + } else { + format!("{name}({})", fields.join(", ")) + } + } + + fn product_field_tys(&mut self, ty: InferTy<'db>) -> Option>> { + product_elems_by(ty, |ty| self.coverage_ty(ty)) + } + + fn coverage_product_pat( + &mut self, + body: FuncBody<'db>, + elems: &[Id>], + field_tys: &[InferTy<'db>], + ) -> Option> { + match elems { + [] => Some(CoveragePat::constant(CoverageHead::Ctor( + CoverageCtor::Builtin(BuiltinCoverageCtor::Unit), + ))), + [elem] => self.coverage_pat(body, *elem, field_tys[0].clone()), + [head, tail @ ..] => { + let head = self.coverage_pat(body, *head, field_tys[0].clone())?; + let tail = self.coverage_product_pat(body, tail, &field_tys[1..])?; + Some(CoveragePat::constructor( + CoverageHead::Ctor(CoverageCtor::Builtin(BuiltinCoverageCtor::Pair)), + [head, tail], + )) + } + } + } + + fn coverage_lit_pat(&mut self, lit: &LitKind, expected: InferTy<'db>) -> CoveragePat<'db> { + match lit { + LitKind::Number(value) => self.numeric_coverage_lit_pat(value, 10, expected), + LitKind::Hex(value) => self.numeric_coverage_lit_pat( + value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + .unwrap_or(value), + 16, + expected, + ), + LitKind::String(value) => { + CoveragePat::constant(CoverageHead::Literal(format!("string:{value}"))) + } + LitKind::Error => unreachable!("error literals are filtered before coverage analysis"), + } + } + + fn numeric_coverage_lit_pat( + &mut self, + digits: &str, + radix: u32, + expected: InferTy<'db>, + ) -> CoveragePat<'db> { + let expected = self.coverage_ty(expected); + let canonical = match expected { + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Word), + args, + } if args.is_empty() => { + canonical_word_unsigned_integer(digits, radix).map(|value| format!("word:{value}")) + } + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Integer), + args, + } if args.is_empty() => canonical_exact_unsigned_integer(digits, radix) + .map(|value| format!("integer:{value}")), + _ => None, + }; + CoveragePat::constant(CoverageHead::Literal( + canonical.unwrap_or_else(|| raw_numeric_literal_key(digits, radix)), + )) + } +} + +/// Keeps duplicate detection for an identical source spelling when earlier +/// errors leave the expected type unavailable, or when programmatically +/// constructed malformed HIR cannot be normalized. The radix is part of the +/// key, so this fallback never guesses that distinct representations have the +/// same value. +fn raw_numeric_literal_key(digits: &str, radix: u32) -> String { + format!("raw:{radix}:{digits}") +} + +/// Converts an unsigned integer to its canonical 256-bit word value. +/// HIR literals originate in the lexer, but returning `None` keeps this helper +/// total for programmatically constructed HIR as well. Fixed limbs match +/// backend word wrapping and keep work linear in the source spelling length. +fn canonical_word_unsigned_integer(digits: &str, radix: u32) -> Option { + if digits.is_empty() || !(2..=16).contains(&radix) { + return None; + } + + let mut limbs = [0u32; 8]; + for ch in digits.chars() { + let digit = ch.to_digit(radix)?; + let mut carry = u64::from(digit); + for limb in &mut limbs { + let value = u64::from(*limb) * u64::from(radix) + carry; + *limb = value as u32; + carry = value >> 32; + } + let _ = carry; + } + + let Some(high_index) = limbs.iter().rposition(|limb| *limb != 0) else { + return Some("0".to_owned()); + }; + let mut canonical = format!("{:x}", limbs[high_index]); + for limb in limbs[..high_index].iter().rev() { + canonical.push_str(&format!("{limb:08x}")); + } + Some(canonical) +} + +fn canonical_exact_unsigned_integer(digits: &str, radix: u32) -> Option { + if digits.is_empty() || !(2..=16).contains(&radix) { + return None; + } + BigUint::parse_bytes(digits.as_bytes(), radix).map(|value| value.to_str_radix(16)) +} + +impl<'db> ConstructorOracle, CoverageHead<'db>> for InferCtx<'db> { + type Error = (); + + fn constructor_space( + &mut self, + ty: &InferTy<'db>, + seen: &[CoverageHead<'db>], + ) -> Result>, Self::Error> { + let ty = self.coverage_ty(ty.clone()); + if let Some(constructors) = self.finite_constructor_space(ty.clone()) { + return Ok(ConstructorSpace::from_finite( + seen, + constructors.into_iter().map(CoverageHead::Ctor), + )); + } + match ty { + InferTy::Named { + ctor: + TyCtor::Builtin( + crate::BuiltinTyCtor::Word + | crate::BuiltinTyCtor::String + | crate::BuiltinTyCtor::Integer, + ), + args, + } if args.is_empty() => Ok(ConstructorSpace::open()), + InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } if self.adt_lookup(def).is_some() => Ok(ConstructorSpace::open()), + _ => Err(()), + } + } + + fn constructor_fields( + &mut self, + ty: &InferTy<'db>, + head: &CoverageHead<'db>, + ) -> Result>, Self::Error> { + match head { + CoverageHead::Literal(_) => Ok(ConstructorFields::Fields(Vec::new())), + CoverageHead::Ctor(ctor) => self + .field_tys_for_ctor(ctor, ty.clone()) + .map(ConstructorFields::Fields) + .ok_or(()), + } + } +} + +#[cfg(test)] +mod literal_key_tests { + use num_bigint::BigUint; + + use super::{ + canonical_exact_unsigned_integer, canonical_word_unsigned_integer, raw_numeric_literal_key, + }; + + #[test] + fn numeric_literal_canonicalization_is_radix_and_width_independent() { + const TWO_256: &str = + "115792089237316195423570985008687907853269984665640564039457584007913129639936"; + assert_eq!( + canonical_word_unsigned_integer("00010", 10), + canonical_word_unsigned_integer("000A", 16) + ); + assert_eq!( + canonical_word_unsigned_integer("340282366920938463463374607431768211455", 10), + Some("ffffffffffffffffffffffffffffffff".to_owned()) + ); + assert_eq!( + canonical_word_unsigned_integer(TWO_256, 10), + canonical_word_unsigned_integer("0", 16) + ); + assert_eq!( + canonical_exact_unsigned_integer("00010", 10), + canonical_exact_unsigned_integer("000A", 16) + ); + assert_ne!( + canonical_exact_unsigned_integer(TWO_256, 10), + canonical_exact_unsigned_integer("0", 16) + ); + let wide = BigUint::from(1u8) << 8_192usize; + let wide_decimal = wide.to_str_radix(10); + let wide_hex = wide.to_str_radix(16); + assert_eq!( + canonical_exact_unsigned_integer(&wide_decimal, 10), + canonical_exact_unsigned_integer(&wide_hex, 16) + ); + assert_eq!(raw_numeric_literal_key("0010", 10), "raw:10:0010"); + assert_ne!( + raw_numeric_literal_key("0010", 10), + raw_numeric_literal_key("10", 10) + ); + assert_ne!( + raw_numeric_literal_key("10", 10), + raw_numeric_literal_key("a", 16) + ); + } +} diff --git a/crates/hir-ty/src/infer/ctx.rs b/crates/hir-ty/src/infer/ctx.rs new file mode 100644 index 00000000..53dec110 --- /dev/null +++ b/crates/hir-ty/src/infer/ctx.rs @@ -0,0 +1,753 @@ +use super::*; +use crate::display::{display_pred_source, display_ty_source, display_type_ref_source}; + +pub(super) enum PoisonTarget<'db> { + Expr(FuncBody<'db>, Id>), + Pat(FuncBody<'db>, Id>), +} + +pub(super) struct InferCtx<'db> { + pub(super) db: &'db dyn Db, + pub(super) lowerer: TypeLowering<'db>, + pub(super) engine: InferTable<'db>, + pub(super) module: Module<'db>, + pub(super) entry_module: Option>, + pub(super) root_body: FuncBody<'db>, + pub(super) root_param_count: usize, + pub(super) root_binder_count: u32, + pub(super) type_vars: Vec>, + pub(super) type_var_names: Vec, + pub(super) expr_resolutions: + FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + pub(super) pat_resolutions: + FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + pub(super) param_tys: FxHashMap<(FuncBody<'db>, u32), InferTy<'db>>, + pub(super) let_tys: FxHashMap<(FuncBody<'db>, Id>), InferTy<'db>>, + pub(super) adt_field_selections: Vec>, + pub(super) pending_conversions: Vec>, + pub(super) pat_tys_for_locals: FxHashMap<(FuncBody<'db>, Id>), InferTy<'db>>, + pub(super) sail_scopes: Vec>>, + pub(super) return_stack: Vec>, + pub(super) return_display_stack: Vec>, + pub(super) expected_expr_displays: FxHashMap<(FuncBody<'db>, Id>), String>, + pub(super) expr_tys: Vec<(FuncBody<'db>, Id>, InferTy<'db>)>, + pub(super) pat_tys: Vec<(FuncBody<'db>, Id>, InferTy<'db>)>, + pub(super) pending: Vec>, + pub(super) comptime_obligations: Vec>, + pub(super) pending_comptime_lets: Vec>, + pub(super) trait_env: Option>, + pub(super) partial_data: Vec<(String, Vec)>, + pub(super) pre_typeck_desugar: Vec>, + pub(super) closure_sigs: FxHashMap, ClosureSig<'db>>, + pub(super) phantom_constructor_results: + FxHashMap<(FuncBody<'db>, Id>), (InferTy<'db>, Vec>)>, + pub(super) integer_literal_pattern_vars: Vec>, + pub(super) reported_ambiguous_constraint: bool, + pub(super) poisoned_exprs: FxHashSet<(FuncBody<'db>, Id>)>, + pub(super) poisoned_pats: FxHashSet<(FuncBody<'db>, Id>)>, + pub(super) diagnostics: Vec, +} + +impl<'db> InferCtx<'db> { + fn new(db: &'db dyn Db, body: FuncBody<'db>, ctx: BodyTyContext<'db>) -> Self { + let module = ctx.module; + let entry_module = ctx.entry_module; + let type_vars = ctx.type_vars; + let type_var_names = type_vars + .iter() + .map(|var| (*var.name.atom()).text(db).to_owned()) + .collect::>(); + let binders = BinderEnv::from_type_vars(&type_vars); + let root_param_count = ctx.params.len(); + let root_binder_count = binders.binder_count(); + let lowerer = TypeLowering::from_body_resolutions(db, &ctx.name_resolution, binders); + let expr_resolutions = ctx + .name_resolution + .exprs + .iter() + .map(|entry| ((entry.body, entry.expr), entry.resolution.clone())) + .collect(); + let pat_resolutions = ctx + .name_resolution + .pats + .iter() + .map(|entry| ((entry.body, entry.pat), entry.resolution.clone())) + .collect(); + let mut engine = InferTable::new(db); + let mut param_tys = FxHashMap::default(); + let mut root_scope = FxHashMap::default(); + for (index, ty) in ctx.params.into_iter().enumerate() { + let infer_ty = engine.from_ty(ty); + param_tys.insert((body, index as u32), infer_ty.clone()); + if let Some(name) = ctx.param_names.get(index) { + root_scope.insert(name.clone(), infer_ty); + } + } + let ret_ty = ctx + .ret + .map(|ty| engine.from_ty(ty)) + .unwrap_or_else(|| engine.fresh_var()); + Self { + db, + lowerer, + engine, + module, + entry_module, + root_body: body, + root_param_count, + root_binder_count, + type_vars, + type_var_names, + expr_resolutions, + pat_resolutions, + param_tys, + let_tys: FxHashMap::default(), + adt_field_selections: Vec::new(), + pending_conversions: Vec::new(), + pat_tys_for_locals: FxHashMap::default(), + sail_scopes: vec![root_scope], + return_stack: vec![ret_ty], + return_display_stack: vec![ctx.ret_display], + expected_expr_displays: FxHashMap::default(), + expr_tys: Vec::new(), + pat_tys: Vec::new(), + pending: Vec::new(), + comptime_obligations: Vec::new(), + pending_comptime_lets: Vec::new(), + trait_env: ctx.trait_env, + partial_data: ctx.partial_data, + pre_typeck_desugar: ctx.pre_typeck_desugar, + closure_sigs: FxHashMap::default(), + phantom_constructor_results: FxHashMap::default(), + integer_literal_pattern_vars: Vec::new(), + reported_ambiguous_constraint: false, + poisoned_exprs: FxHashSet::default(), + poisoned_pats: FxHashSet::default(), + diagnostics: Vec::new(), + } + } + + fn finish(mut self) -> InferenceResult<'db> { + let solved = if let Some(trait_env) = self.trait_env { + self.solve_pending_obligations(trait_env) + } else { + ObligationSolveOutput::default() + }; + self.default_integer_literal_patterns(); + if self.diagnostics.is_empty() { + self.check_ambiguous_constructor_results(); + } + if self.diagnostics.is_empty() { + self.check_ambiguous_integer_literals(); + } + self.default_root_integer_literals(); + let checked_conversions = self.finish_pending_conversions(); + let poisoned_exprs = self.poisoned_exprs.clone(); + let poisoned_pats = self.poisoned_pats.clone(); + let root_scheme = self.inferred_root_scheme(); + let expr_tys = self + .expr_tys + .into_iter() + .map(|(body, expr, ty)| ExprTy { + body, + expr, + ty: self + .engine + .ground_ty(if poisoned_exprs.contains(&(body, expr)) { + InferTy::Error + } else { + ty + }), + }) + .collect(); + let pat_tys = self + .pat_tys + .into_iter() + .map(|(body, pat, ty)| PatTy { + body, + pat, + ty: self + .engine + .ground_ty(if poisoned_pats.contains(&(body, pat)) { + InferTy::Error + } else { + ty + }), + }) + .collect(); + let let_tys = self + .let_tys + .into_iter() + .map(|((body, stmt), ty)| LetTy { + body, + stmt, + ty: self.engine.ground_ty(ty), + }) + .collect(); + let obligations = self + .pending + .into_iter() + .map(|pending| { + let main = self.engine.ground_ty(pending.main); + let args = pending + .args + .into_iter() + .map(|arg| self.engine.ground_ty(arg)) + .collect(); + DeferredObligation { + pred: Pred::in_class(self.db, pending.class, main, args), + source: pending.source, + } + }) + .collect(); + let mut comptime_obligations = self.comptime_obligations; + for pending in self.pending_comptime_lets { + let ty = self.engine.ground_ty(pending.ty); + if pending.declared || ty_requires_comptime(self.db, ty) { + comptime_obligations.push(ComptimeObligation { + body: pending.body, + expr: pending.expr, + kind: ComptimeObligationKind::LetInit { + stmt: pending.stmt, + name: pending.name, + }, + }); + } + } + let mut result = InferenceResult { + root_scheme, + expr_tys, + pat_tys, + let_tys, + adt_field_selections: self.adt_field_selections, + checked_conversions, + obligations, + obligation_evidence: solved.evidence, + call_site_evidence: solved.call_site_evidence, + comptime_obligations, + diagnostics: self.diagnostics, + }; + result.diagnostics.extend(solved.diagnostics); + result + } + + fn finish_pending_conversions(&mut self) -> Vec> { + let pending = std::mem::take(&mut self.pending_conversions); + if pending.is_empty() { + return Vec::new(); + } + let mut checked = Vec::with_capacity(pending.len()); + let mut item_resolutions = None; + for conversion in pending { + if self + .poisoned_exprs + .contains(&(conversion.body, conversion.expr)) + { + continue; + } + let source = self.normalize_aliases(conversion.source); + let target = self.normalize_aliases(conversion.target); + let source = self.engine.ground_ty(source); + let target = self.engine.ground_ty(target); + if !ty_is_resolved_for_conversion(self.db, source) + || !ty_is_resolved_for_conversion(self.db, target) + { + continue; + } + if source == target { + checked.push(CheckedConversion { + body: conversion.body, + expr: conversion.expr, + source, + target, + kind: ConversionKind::Identity, + }); + continue; + } + let value_type_conversion = if value_type_def(self.db, source).is_some() + || value_type_def(self.db, target).is_some() + { + let item_resolutions = + item_resolutions.get_or_insert_with(|| self.item_resolutions_for_aliases()); + self.value_type_conversion_kind(item_resolutions, source, target) + } else { + None + }; + if let Some(kind) = value_type_conversion { + checked.push(CheckedConversion { + body: conversion.body, + expr: conversion.expr, + source, + target, + kind, + }); + continue; + } + self.diagnostics.push(TypeckDiagnostic::InvalidConversion { + target_span: self.label_span(conversion.target_ref.span(self.db)), + operand_span: self.expr_label_span(conversion.body, conversion.operand), + source: display_ty_source(self.db, source, &self.type_var_names), + target: display_ty_source(self.db, target, &self.type_var_names), + }); + } + checked + } + + fn value_type_conversion_kind( + &self, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + source: Ty<'db>, + target: Ty<'db>, + ) -> Option { + if let Some(def) = value_type_def(self.db, target) + && value_type_underlying_in_context(self.db, self.module, item_resolutions, def) + .is_ok_and(|underlying| underlying == source) + { + return Some(ConversionKind::ValueTypeWrap); + } + if let Some(def) = value_type_def(self.db, source) + && value_type_underlying_in_context(self.db, self.module, item_resolutions, def) + .is_ok_and(|underlying| underlying == target) + { + return Some(ConversionKind::ValueTypeUnwrap); + } + None + } + + fn inferred_root_scheme(&mut self) -> TyScheme<'db> { + let params = (0..self.root_param_count) + .map(|index| { + self.param_tys + .get(&(self.root_body, index as u32)) + .cloned() + .unwrap_or(InferTy::Error) + }) + .collect::>(); + let ret = self.return_stack.first().cloned().unwrap_or(InferTy::Error); + let mut generalizer = + InferredSchemeGeneralizer::new(self.db, &mut self.engine, self.root_binder_count); + let ty = generalizer.ty(InferTy::Function { + params, + ret: Box::new(ret), + }); + TyScheme::new( + self.db, + generalizer.binder_count(), + QualTy::monotype(self.db, ty), + ) + } + + pub(super) fn param_ty(&mut self, body: FuncBody<'db>, index: u32) -> InferTy<'db> { + if let Some(ty) = self.param_tys.get(&(body, index)) { + return ty.clone(); + } + let ty = self.engine.fresh_var(); + self.param_tys.insert((body, index), ty.clone()); + ty + } + + pub(super) fn let_ty(&mut self, body: FuncBody<'db>, stmt: Id>) -> InferTy<'db> { + if let Some(ty) = self.let_tys.get(&(body, stmt)) { + return ty.clone(); + } + let ty = self.engine.fresh_var(); + self.let_tys.insert((body, stmt), ty.clone()); + ty + } + + pub(super) fn pattern_local_ty( + &mut self, + body: FuncBody<'db>, + pat: Id>, + ) -> InferTy<'db> { + if let Some(ty) = self.pat_tys_for_locals.get(&(body, pat)) { + return ty.clone(); + } + let ty = self.engine.fresh_var(); + self.pat_tys_for_locals.insert((body, pat), ty.clone()); + ty + } + + pub(super) fn maybe_comptime( + &mut self, + marker: Option>, + ty: InferTy<'db>, + ) -> InferTy<'db> { + if marker.is_none() || matches!(self.engine.resolve(ty.clone()), InferTy::Comptime(_)) { + ty + } else { + InferTy::Comptime(Box::new(ty)) + } + } + + pub(super) fn is_numeric_or_open(&mut self, ty: InferTy<'db>) -> bool { + let ty = self.normalize_aliases(ty); + match self.engine.resolve(ty) { + InferTy::Error | InferTy::Unknown | InferTy::Var(_) => true, + InferTy::Comptime(inner) => self.is_numeric_or_open(*inner), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Word | crate::BuiltinTyCtor::Integer), + args, + } => args.is_empty(), + _ => false, + } + } + + pub(super) fn body_context(&self, body: FuncBody<'db>) -> String { + body.def_id(self.db) + .name(self.db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| "lambda".to_owned()) + } + + pub(super) fn display_infer_ty(&mut self, ty: InferTy<'db>) -> String { + self.engine.display_with_names(ty, &self.type_var_names) + } + + pub(super) fn display_pred(&self, pred: Pred<'db>) -> String { + display_pred_source(self.db, pred, &self.type_var_names) + } + + pub(super) fn display_type_ref(&self, ty: TypeRef<'db>) -> String { + display_type_ref_source(self.db, ty) + } + + pub(super) fn desugar_view(&self) -> BodyDesugarView<'_, 'db> { + BodyDesugarView::new(&self.pre_typeck_desugar) + } + + pub(super) fn tuple_expr_product_shape( + &self, + body: FuncBody<'db>, + expr: Id>, + elems: &[Id>], + ) -> ProductShape>> { + self.desugar_view() + .tuple_expr_product(body, expr) + .cloned() + .unwrap_or_else(|| ProductShape::from_slice(elems)) + } + + pub(super) fn tuple_pat_product_shape( + &self, + body: FuncBody<'db>, + pat: Id>, + elems: &[Id>], + ) -> ProductShape>> { + self.desugar_view() + .tuple_pat_product(body, pat) + .cloned() + .unwrap_or_else(|| ProductShape::from_slice(elems)) + } + + pub(super) fn if_stmt_match_input( + &self, + body: FuncBody<'db>, + stmt: Id>, + cond: Id>, + then_body: &[Id>], + else_body: Option<&[Id>]>, + ) -> IfStmtMatchInput<'db> { + if_stmt_match_input(self.desugar_view(), body, stmt, cond, then_body, else_body) + } + + pub(super) fn if_expr_match_input( + &self, + body: FuncBody<'db>, + expr: Id>, + cond: Id>, + then_expr: Id>, + else_expr: Id>, + ) -> IfExprMatchInput<'db> { + if_expr_match_input(self.desugar_view(), body, expr, cond, then_expr, else_expr) + } + + pub(super) fn bool_expr_unit_sum( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option> { + self.desugar_view().bool_expr_unit_sum(body, expr) + } + + pub(super) fn bool_pat_unit_sum( + &self, + body: FuncBody<'db>, + pat: Id>, + ) -> Option> { + self.desugar_view().bool_pat_unit_sum(body, pat) + } + + pub(super) fn label_span(&self, span: Span<'db>) -> LabelSpan { + self.diagnostic_sources().label_span(span) + } + + pub(super) fn diagnostic_sources(&self) -> DiagnosticSourceMap<'_, 'db> { + DiagnosticSourceMap::new(self.db, &self.pre_typeck_desugar) + } + + pub(super) fn unit(&mut self) -> InferTy<'db> { + self.engine.from_ty(Ty::unit(self.db)) + } + + pub(super) fn word(&mut self) -> InferTy<'db> { + self.engine.from_ty(Ty::word(self.db)) + } + + pub(super) fn bool(&mut self) -> InferTy<'db> { + self.engine.from_ty(Ty::bool(self.db)) + } + + pub(super) fn string(&mut self) -> InferTy<'db> { + self.engine.from_ty(Ty::string(self.db)) + } + + pub(super) fn poison_expr(&mut self, body: FuncBody<'db>, expr: Id>) { + self.poisoned_exprs.insert((body, expr)); + } + + pub(super) fn poison_pat(&mut self, body: FuncBody<'db>, pat: Id>) { + self.poisoned_pats.insert((body, pat)); + } + + pub(super) fn emit_expr_error( + &mut self, + body: FuncBody<'db>, + expr: Id>, + diagnostic: TypeckDiagnostic, + ) { + self.emit_error_with_poison(diagnostic, [PoisonTarget::Expr(body, expr)]); + } + + pub(super) fn emit_pat_error( + &mut self, + body: FuncBody<'db>, + pat: Id>, + diagnostic: TypeckDiagnostic, + ) { + self.emit_error_with_poison(diagnostic, [PoisonTarget::Pat(body, pat)]); + } + + pub(super) fn emit_error_with_poison(&mut self, diagnostic: TypeckDiagnostic, targets: I) + where + I: IntoIterator>, + { + self.diagnostics.push(diagnostic); + for target in targets { + match target { + PoisonTarget::Expr(body, expr) => self.poison_expr(body, expr), + PoisonTarget::Pat(body, pat) => self.poison_pat(body, pat), + } + } + } + + pub(super) fn expr_is_poisoned(&self, body: FuncBody<'db>, expr: Id>) -> bool { + self.poisoned_exprs.contains(&(body, expr)) + } + + pub(super) fn pat_is_poisoned(&self, body: FuncBody<'db>, pat: Id>) -> bool { + self.poisoned_pats.contains(&(body, pat)) + } + + pub(super) fn body_label_span(&self, body: FuncBody<'db>) -> LabelSpan { + self.label_span(body.span(self.db)) + } + + pub(super) fn obligation_source_label_span(&self, source: &ObligationSource<'db>) -> LabelSpan { + match source { + ObligationSource::IntegerLiteral { body, expr } + | ObligationSource::ClassMethod { body, expr } => self.expr_label_span(*body, *expr), + ObligationSource::CallSite { + body, call_expr, .. + } => self.expr_label_span(*body, *call_expr), + ObligationSource::IntegerLiteralPattern { body, pat } => { + self.pat_label_span(*body, *pat) + } + ObligationSource::Scheme => self.label_span(self.module.span(self.db)), + } + } + + pub(super) fn unsatisfied_constraint_label_span( + &self, + source: &ObligationSource<'db>, + pred: Pred<'db>, + ) -> LabelSpan { + self.pred_type_var_label_span(pred) + .unwrap_or_else(|| self.obligation_source_label_span(source)) + } + + fn pred_type_var_label_span(&self, pred: Pred<'db>) -> Option { + match pred.kind(self.db) { + PredKind::InClass { main, args, .. } => { + self.ty_type_var_label_span(*main).or_else(|| { + args.iter() + .find_map(|arg| self.ty_type_var_label_span(*arg)) + }) + } + PredKind::Eq { lhs, rhs } => self + .ty_type_var_label_span(*lhs) + .or_else(|| self.ty_type_var_label_span(*rhs)), + PredKind::Error => None, + } + } + + fn ty_type_var_label_span(&self, ty: Ty<'db>) -> Option { + match ty.kind(self.db) { + TyKind::BoundVar(var) => self + .type_vars + .get(var.index as usize) + .map(|binding| self.label_span(binding.name.span(self.db))), + TyKind::Named { args, .. } | TyKind::Tuple(args) => args + .iter() + .find_map(|arg| self.ty_type_var_label_span(*arg)), + TyKind::Function { params, ret } => params + .iter() + .find_map(|param| self.ty_type_var_label_span(*param)) + .or_else(|| self.ty_type_var_label_span(*ret)), + TyKind::Comptime(inner) => self.ty_type_var_label_span(*inner), + TyKind::Error | TyKind::Unknown => None, + } + } + + pub(super) fn stmt_label_span(&self, body: FuncBody<'db>, stmt: Id>) -> LabelSpan { + self.diagnostic_sources().stmt_label_span(body, stmt) + } + + pub(super) fn expr_label_span(&self, body: FuncBody<'db>, expr: Id>) -> LabelSpan { + self.diagnostic_sources().expr_label_span(body, expr) + } + + pub(super) fn field_label_span(&self, body: FuncBody<'db>, expr: Id>) -> LabelSpan { + match &body.exprs(self.db).get(expr).kind { + ExprKind::Field { field, .. } => self.label_span(field.span(self.db)), + _ => self.expr_label_span(body, expr), + } + } + + pub(super) fn pat_label_span(&self, body: FuncBody<'db>, pat: Id>) -> LabelSpan { + self.diagnostic_sources().pat_label_span(body, pat) + } + + pub(super) fn yul_stmt_label_span(&self, stmt: &YulStmt<'db>) -> LabelSpan { + self.label_span(stmt.span(self.db)) + } + + pub(super) fn yul_expr_label_span(&self, expr: &YulExpr<'db>) -> LabelSpan { + self.label_span(expr.span(self.db)) + } + + pub(super) fn comptime_callee_name( + &self, + body: FuncBody<'db>, + callee: Id>, + ) -> String { + match &body.exprs(self.db).get(callee).kind { + ExprKind::Ident(name) => (*name.atom()).text(self.db).to_owned(), + ExprKind::Field { field, .. } => (*field.atom()).text(self.db).to_owned(), + _ => "callee".to_owned(), + } + } + + pub(super) fn is_namespace_expr(&self, body: FuncBody<'db>, expr: Id>) -> bool { + matches!( + self.expr_resolutions.get(&(body, expr)), + Some( + hir_nameres::Resolution::Def { + kind: hir_nameres::DefResolutionKind::Adt + | hir_nameres::DefResolutionKind::Contract + | hir_nameres::DefResolutionKind::Class + | hir_nameres::DefResolutionKind::TypeAlias + | hir_nameres::DefResolutionKind::ValueType, + .. + } | hir_nameres::Resolution::Builtin( + hir_nameres::BuiltinKind::Type(_) | hir_nameres::BuiltinKind::Class(_) + ) | hir_nameres::Resolution::Module(_) + ) + ) + } + + pub(super) fn field_name(&self, body: FuncBody<'db>, expr: Id>) -> String { + match &body.exprs(self.db).get(expr).kind { + ExprKind::Field { field, .. } => (*field.atom()).text(self.db).to_owned(), + _ => "".to_owned(), + } + } + + pub(super) fn push_sail_scope(&mut self) { + self.sail_scopes.push(FxHashMap::default()); + } + + pub(super) fn pop_sail_scope(&mut self) { + self.sail_scopes.pop(); + if self.sail_scopes.is_empty() { + self.sail_scopes.push(FxHashMap::default()); + } + } + + pub(super) fn add_sail_local(&mut self, name: String, ty: InferTy<'db>) { + if let Some(scope) = self.sail_scopes.last_mut() { + scope.insert(name, ty); + } + } + + pub(super) fn lookup_sail_local(&self, name: &str) -> Option> { + self.sail_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + } +} + +fn value_type_def<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Option> { + match ty.kind(db) { + TyKind::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::ValueType, + }), + args, + } if args.is_empty() => Some(*def), + _ => None, + } +} + +#[salsa::tracked] +#[tracing::instrument( + target = "hir_ty::query", + level = "debug", + skip(db, body, ctx), + fields(file = field::Empty, def = field::Empty) +)] +pub fn infer_body<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + ctx: BodyTyContext<'db>, +) -> InferenceResult<'db> { + if tracing::enabled!(target: "hir_ty::query", tracing::Level::DEBUG) { + let def = body.def_id(db); + let span = tracing::Span::current(); + span.record("file", field::display(file_url_tail(db, def.file(db)))); + span.record( + "def", + field::display( + def.name(db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("{:?}", def.kind(db))), + ), + ); + } + let mut infer = InferCtx::new(db, body, ctx); + infer.infer_body(body); + infer.finish() +} + +/// Returns type-checking diagnostics for one body. +#[salsa::tracked(returns(ref))] +pub fn body_ty_diagnostics<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + ctx: BodyTyContext<'db>, +) -> Vec { + infer_body(db, body, ctx).diagnostics +} diff --git a/crates/hir-ty/src/infer/desugar_view.rs b/crates/hir-ty/src/infer/desugar_view.rs new file mode 100644 index 00000000..8b0658ac --- /dev/null +++ b/crates/hir-ty/src/infer/desugar_view.rs @@ -0,0 +1,127 @@ +use super::*; + +pub(super) struct IfStmtMatchInput<'db> { + pub(super) cond: Id>, + pub(super) then_body: Vec>>, + pub(super) else_body: Option>>>, +} + +pub(super) struct IfExprMatchInput<'db> { + pub(super) cond: Id>, + pub(super) then_expr: Id>, + pub(super) else_expr: Id>, +} + +pub(super) fn if_stmt_match_input<'db>( + view: BodyDesugarView<'_, 'db>, + body: FuncBody<'db>, + stmt: Id>, + cond: Id>, + then_body: &[Id>], + else_body: Option<&[Id>]>, +) -> IfStmtMatchInput<'db> { + view.if_stmt_match(body, stmt) + .map(|view| IfStmtMatchInput { + cond: view.cond, + then_body: view.then_body.to_vec(), + else_body: view.else_body.map(|body| body.to_vec()), + }) + .unwrap_or_else(|| IfStmtMatchInput { + cond, + then_body: then_body.to_vec(), + else_body: else_body.map(|body| body.to_vec()), + }) +} + +pub(super) fn if_expr_match_input<'db>( + view: BodyDesugarView<'_, 'db>, + body: FuncBody<'db>, + expr: Id>, + cond: Id>, + then_expr: Id>, + else_expr: Id>, +) -> IfExprMatchInput<'db> { + view.if_expr_match(body, expr) + .map(|view| IfExprMatchInput { + cond: view.cond, + then_expr: view.then_expr, + else_expr: view.else_expr, + }) + .unwrap_or(IfExprMatchInput { + cond, + then_expr, + else_expr, + }) +} + +pub(super) fn product_infer_ty_from_shape<'db>(shape: &ProductShape>) -> InferTy<'db> { + match shape { + ProductShape::Unit => unit_infer_ty(), + ProductShape::Single(elem) => elem.clone(), + ProductShape::Pair { head, tail } => InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args: vec![head.clone(), product_infer_ty_from_shape(tail)], + }, + } +} + +pub(super) fn product_infer_ty<'db>(elems: Vec>) -> InferTy<'db> { + product_infer_ty_from_shape(&ProductShape::from_slice(&elems)) +} + +pub(super) fn unit_infer_ty<'db>() -> InferTy<'db> { + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args: Vec::new(), + } +} + +pub(super) fn product_elems<'db>( + engine: &mut InferTable<'db>, + ty: InferTy<'db>, +) -> Option>> { + product_elems_by(ty, |ty| engine.resolve(ty)) +} + +pub(super) fn product_elems_by<'db, F>( + ty: InferTy<'db>, + mut resolve: F, +) -> Option>> +where + F: FnMut(InferTy<'db>) -> InferTy<'db>, +{ + match resolve(ty) { + InferTy::Tuple(elems) => Some(elems), + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Some(Vec::new()), + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => { + let mut elems = Vec::new(); + elems.push(args[0].clone()); + push_product_tail_by(args[1].clone(), &mut elems, &mut resolve); + Some(elems) + } + _ => None, + } +} + +fn push_product_tail_by<'db, F>(ty: InferTy<'db>, out: &mut Vec>, resolve: &mut F) +where + F: FnMut(InferTy<'db>) -> InferTy<'db>, +{ + match resolve(ty.clone()) { + InferTy::Tuple(elems) => out.extend(elems), + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => { + out.push(args[0].clone()); + push_product_tail_by(args[1].clone(), out, resolve); + } + _ => out.push(ty), + } +} diff --git a/crates/hir-ty/src/infer/diagnostics.rs b/crates/hir-ty/src/infer/diagnostics.rs new file mode 100644 index 00000000..4a4f6d64 --- /dev/null +++ b/crates/hir-ty/src/infer/diagnostics.rs @@ -0,0 +1,2461 @@ +use super::*; +use crate::display::{ + display_ty_return_suffix, display_ty_source, display_type_ref_return_suffix, + display_type_ref_source, +}; + +/// User-facing information about a callable definition. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct CalleeDiagnostic { + /// Callable display name. + pub name: String, + /// Source-style signature. + pub signature: String, + /// Definition span, when the callable has a source definition. + pub definition: Option, +} + +/// User-facing information about a callable parameter. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ParameterDiagnostic { + /// Zero-based parameter index. + pub index: usize, + /// Parameter name, when the source declaration has one. + pub name: Option, + /// Source spelling for the parameter type, when available. + pub ty: Option, + /// Parameter definition span, when available. + pub definition: Option, +} + +/// Typed type-checking diagnostic. +/// +/// Diagnostics store display-string type snapshots so they are lifetime-free +/// and do not expose ephemeral inference variables after inference finishes. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum TypeckDiagnostic { + /// `SC0201`: two types could not be unified. + Mismatch { + /// Source span for the expression or pattern whose type mismatched. + span: LabelSpan, + /// Expected or left-hand type snapshot. + expected: String, + /// Actual or right-hand type snapshot. + actual: String, + }, + /// `SC0201`: an argument does not match the resolved callee parameter. + ArgMismatch { + /// Source span for the argument whose type mismatched. + span: LabelSpan, + /// Expected parameter type snapshot. + expected: String, + /// Actual argument type snapshot. + actual: String, + /// Callee information, when the call resolved to a known callable. + callee: Option, + /// Parameter information for the mismatched argument. + param: ParameterDiagnostic, + }, + /// `SC0202`: unification would create an infinite type. + OccursCheck { + /// Source span where the recursive type was required. + span: LabelSpan, + /// Inference variable snapshot. + var: String, + /// Type snapshot containing the variable. + ty: String, + }, + /// `SC0299`: inferred constraints mention variables not determined by the + /// inferred function type. + AmbiguousInferredType { + /// Source span for the ambiguous definition. + span: LabelSpan, + /// Generalized inferred type snapshot. + scheme: String, + }, + /// `SC0299`: a type constructor was applied to the wrong number of type + /// arguments. + TypeConstructorArity { + /// Source span for the ill-kinded type annotation. + span: LabelSpan, + /// Type constructor name. + constructor: String, + /// Full type annotation snapshot. + ty: String, + /// Declared arity. + expected: usize, + /// Actual argument count. + actual: usize, + }, + /// `SC0102`: a trait or impl head relies on a type variable that was not + /// declared by an explicit generic parameter list. + UndefinedTypeVariables { + /// Undeclared variables with their source spans. + vars: Vec<(LabelSpan, String)>, + }, + /// `SC0203`: function, constructor, or match arm arity mismatch. + WrongArity { + /// Source span for the call, constructor, signature, or syntactic + /// context. + span: LabelSpan, + /// Callable or syntactic context. + context: String, + /// Expected number of arguments/patterns. + expected: usize, + /// Actual number of arguments/patterns. + actual: usize, + /// Callee information for call-like arity errors. + callee: Option, + }, + /// `SC0203`: mutually recursive data declarations are rejected by the + /// reference frontend. + MutualRecursiveData { + /// Source span for one cross-recursive type reference. + span: LabelSpan, + /// Referenced type that would be unavailable in the reference order. + ty: String, + }, + /// `SC0204`: a SAIL variable referenced by Yul is not word-typed. + NonWordYulVar { + /// Source span for the Yul reference. + span: LabelSpan, + /// Referenced SAIL variable name. + name: String, + /// Actual type snapshot. + actual: String, + }, + /// `SC0205`: field lookup could not be typed. + UnknownField { + /// Source span for the field projection. + span: LabelSpan, + /// Field name. + field: String, + }, + /// `SC0206`: attempted to call a non-function value. + NonCallable { + /// Source span for the attempted call. + span: LabelSpan, + /// Callee type snapshot. + callee: String, + }, + /// `SC0228`: a non-value namespace item appeared in value position. + NamespaceAsValue { + /// Source span for the invalid value occurrence. + span: LabelSpan, + /// Name used in value position. + name: String, + /// Namespace that the name belongs to. + namespace: ValueNamespace, + /// Value-position context. + position: ValuePosition, + }, + /// `SC0229`: a class name appeared where a type was required. + ClassAsType { + /// Source span for the class name. + span: LabelSpan, + /// Class name. + class: String, + }, + /// `SC0229`: a generated dispatch type collides with a user type. + DuplicateType { + /// Source span for the duplicate type. + span: LabelSpan, + /// Type name. + name: String, + /// Span of the prior/generated definition source, when available. + previous: Option, + }, + /// `SC0207`: a class constraint could not be solved. + UnsatisfiedConstraint { + /// Source span for the obligation that could not be solved. + span: LabelSpan, + /// Predicate snapshot. + pred: String, + }, + /// `SC0208`: more than one non-default instance solved a class constraint. + AmbiguousConstraint { + /// Source span for the ambiguous obligation. + span: LabelSpan, + /// Predicate snapshot. + pred: String, + /// Candidate evidence snapshots. + candidates: Vec, + }, + /// `SC0209`: trait solving exceeded its fuel bound. + SolverFuelExhausted { + /// Source span for the obligation that exhausted solver fuel. + span: LabelSpan, + /// Predicate snapshot. + pred: String, + }, + /// `SC0222`: a `return` appears before the final statement in a body. + NonFinalReturn { + /// Source span for the non-final return statement. + span: LabelSpan, + }, + /// `SC0211`: a Yul identifier or function name could not be resolved. + UnknownYulName { + /// Source span for the unknown Yul identifier or function. + span: LabelSpan, + /// Referenced Yul name. + name: String, + }, + /// `SC0212`: weak instance-head variables are not determined by the main + /// type. + CoverageCondition { + /// Source span for the instance head. + span: LabelSpan, + /// Class whose instance violates coverage. + class: String, + /// Main instance-head type snapshot. + main: String, + /// Type variables that appear only in weak class arguments. + undetermined: Vec, + }, + /// `SC0213`: an instance context predicate is not smaller than the head. + PattersonCondition { + /// Source span for the instance head. + span: LabelSpan, + /// Instance-head predicate snapshot. + head: String, + }, + /// `SC0214`: an instance context mentions variables absent from the head. + BoundedVariableCondition { + /// Source span for the instance head. + span: LabelSpan, + }, + /// `SC0215`: a recursive type alias was rejected. + TypeAliasCycle { + /// Source span for the alias declaration. + span: LabelSpan, + /// Alias name. + alias: String, + }, + /// `SC0216`: a type alias was applied with the wrong number of arguments. + TypeAliasArity { + /// Source span for the alias use or declaration. + span: LabelSpan, + /// Alias name. + alias: String, + /// Declared arity. + expected: usize, + /// Actual argument count. + actual: usize, + }, + /// `SC0243`: type alias expansion exceeded the normalizer's node budget. + TypeAliasExpansionLimit { + /// Source span for the alias declaration or use. + span: LabelSpan, + /// Maximum number of type nodes visited while expanding aliases. + limit: usize, + }, + /// `SC0217`: a class predicate used the wrong number of weak arguments. + ClassArity { + /// Source span for the class predicate. + span: LabelSpan, + /// Class name. + class: String, + /// Declared weak-argument arity. + expected: usize, + /// Actual weak-argument count. + actual: usize, + }, + /// `SC0218`: two visible non-default instance heads overlap. + OverlappingInstance { + /// Source span for the later instance head. + instance_span: LabelSpan, + /// Source span for the earlier overlapping instance head, when + /// available. + overlaps_span: Option, + /// New instance predicate. + instance: String, + /// Prior overlapping instance predicate. + overlaps: String, + }, + /// `SC0219`: a default instance main argument contained no type variable. + InvalidDefaultInstance { + /// Source span for the instance head. + span: LabelSpan, + /// Instance predicate snapshot. + head: String, + }, + /// `SC0244`: an instance omits one or more required methods. + /// + /// Reference `SC0220` is the incomplete-signature diagnostic. Older + /// solcore-rs used `SC0220` for incomplete instances; keep the local + /// mapping explicit so the registry does not collide again. + IncompleteInstance { + /// Source span for the instance declaration. + span: LabelSpan, + /// Class name. + class: String, + /// Missing method names. + missing: Vec, + }, + /// `SC0202`: an instance defines a method not declared by the class. + UnknownInstanceMethod { + /// Source span for the extra method name. + span: LabelSpan, + /// Qualified method name as the reference reports it. + name: String, + /// Span of the class definition that declares the valid methods. + class_span: Option, + }, + /// `SC0220`: a top-level or contract function has an incomplete signature. + IncompleteSignature { + /// Source span for the function name. + span: LabelSpan, + /// Source-level signature snapshot. + signature: String, + }, + /// `SC0221`: a class or instance method has an incomplete signature. + IncompleteMethodSignature { + /// Source span for the method name. + span: LabelSpan, + /// Source-level signature snapshot. + signature: String, + }, + /// `SC0221`: an instance method signature does not match its class method. + InvalidInstanceMethodSignature { + /// Source span for the invalid method signature. + span: LabelSpan, + /// Method name. + method: String, + /// Failure reason. + reason: String, + }, + /// `SC0222`: constructor-shaped pattern syntax did not resolve to a + /// constructor. + InvalidConstructorPattern { + /// Source span for the invalid constructor pattern. + span: LabelSpan, + /// Constructor syntax name. + name: String, + }, + /// `SC0223`: matching a partial imported data type needs a catch-all arm. + HiddenConstructorCoverage { + /// Source span for the match that needs a catch-all arm. + span: LabelSpan, + /// Data type being matched. + ty: String, + }, + /// `SC0224`: shorthand constructor lookup failed. + ShorthandConstructor { + /// Source span for the shorthand constructor. + span: LabelSpan, + /// Constructor leaf name. + name: String, + /// Lookup failure reason. + reason: String, + }, + /// `SC0227`: a type has both an auto-derived and manual `Generic` instance. + GenericDeriveConflict { + /// Source span for the ADT declaration. + span: LabelSpan, + /// Type name with the conflicting manual instance. + ty: String, + }, + /// `SC0240`: a runtime expression was supplied to a comptime parameter. + RuntimeToComptimeParam { + /// Source span for the runtime argument. + span: LabelSpan, + /// Callee name. + function: String, + /// Parameter name. + param: String, + }, + /// `SC0241`: a comptime let binding has a runtime initializer. + ComptimeLetRuntime { + /// Source span for the runtime initializer. + span: LabelSpan, + /// Binding name. + name: String, + }, + /// `SC0242`: a function with a comptime return type returns runtime data. + ComptimeReturnRuntime { + /// Source span for the runtime return expression. + span: LabelSpan, + /// Function or body context. + context: String, + }, + /// `SC0245`: a surface conversion has no defined semantics. + InvalidConversion { + /// Conversion target type span. + target_span: LabelSpan, + /// Operand expression span. + operand_span: LabelSpan, + /// Alias-normalized source type. + source: String, + /// Alias-normalized target type. + target: String, + }, + /// `SC0245`: a conversion was used as an assignment location. + ConversionAssignmentTarget { + /// Conversion expression within the assignment target. + span: LabelSpan, + }, + /// `SC0246`: a named field was projected from a contract-storage value. + UnsupportedStorageFieldProjection { + /// Source span for the unsupported field projection. + span: LabelSpan, + /// Projected field name. + field: String, + }, + /// `SC0247`: a user-defined value type has no valid elementary + /// representation. + InvalidValueTypeDefinition { + /// Value-type declaration span. + span: LabelSpan, + /// Declared value-type name. + name: String, + /// Semantic restriction that was violated. + reason: String, + }, + /// `SC0248`: a user-defined value type has a storage representation that + /// the backend cannot encode and decode safely. + UnsupportedValueTypeStorage { + /// Source span for the storage read or write. + span: LabelSpan, + /// User-defined value-type name. + ty: String, + }, + /// `SC0249`: a fixed-length array was read from or written to contract + /// storage before the backend has a sound layout for it. + UnsupportedFixedArrayStorage { + /// Source span for the storage read or write. + span: LabelSpan, + /// Fixed-length array type. + ty: String, + }, + /// `SC0302`: a match does not cover every possible scrutinee value. + NonExhaustiveMatch { + /// Source span for the match scrutinee. + span: LabelSpan, + /// One uncovered pattern row. + missing: String, + }, + /// `SC0303`: a match arm is covered by previous arms. + UnreachableMatchArm { + /// Source span for the unreachable arm. + span: LabelSpan, + }, +} + +/// Non-value namespace used as a value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValueNamespace { + /// Type constructor namespace. + Type, + /// Type class namespace. + Class, + /// Module namespace. + Module, + /// Type-variable namespace. + TypeVariable, +} + +/// Expression context for namespace-as-value diagnostics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValuePosition { + /// Ordinary expression position. + Value, + /// Callee of a call expression. + Callee, +} + +impl TypeckDiagnostic { + /// Lowers this typed diagnostic to the generic rendering surface. + pub fn lower(&self) -> Diagnostic { + match self { + TypeckDiagnostic::Mismatch { + span, + expected, + actual, + } => { + Diagnostic::error(format!("type mismatch: expected {expected}, found {actual}")) + .with_code(DiagnosticCode::TYPECK_MISMATCH) + .with_primary_label_span(span.clone(), Some("expression has mismatched type")) + .with_note(format!("expected type: {expected}")) + .with_note(format!("found type: {actual}")) + } + TypeckDiagnostic::ArgMismatch { + span, + expected, + actual, + callee, + param, + } => { + let param_name = parameter_display(param); + let expected_display = param.ty.as_deref().unwrap_or(expected.as_str()); + let mut diagnostic = if let Some(callee) = callee { + Diagnostic::error(format!( + "argument type mismatch in call to `{}`", + callee.name + )) + .with_code(DiagnosticCode::TYPECK_MISMATCH) + .with_primary_label_span(span.clone(), Some("argument has mismatched type")) + .with_note(format!( + "expected `{expected_display}` because {param_name} of `{}` has type `{expected_display}`", + callee.name + )) + .with_note(format!("found type: {actual}")) + .with_note(format!( + "`{}` has signature `{}`", + callee.name, callee.signature + )) + } else { + Diagnostic::error(format!( + "argument type mismatch: expected {expected}, found {actual}" + )) + .with_code(DiagnosticCode::TYPECK_MISMATCH) + .with_primary_label_span(span.clone(), Some("argument has mismatched type")) + .with_note(format!("expected type: {expected}")) + .with_note(format!("found type: {actual}")) + }; + if let Some(label) = param.definition.clone().or_else(|| { + callee + .as_ref() + .and_then(|callee| callee.definition.clone()) + }) { + diagnostic = diagnostic.with_secondary_label_span( + label, + Some(parameter_definition_label(param, callee.as_ref())), + ); + } + diagnostic + } + TypeckDiagnostic::OccursCheck { span, var, ty } => { + Diagnostic::error("recursive type would be required") + .with_code(DiagnosticCode::TYPECK_RECURSIVE_TYPE_OR_UNKNOWN_INSTANCE_METHOD) + .with_primary_label_span(span.clone(), Some("recursive type required here")) + .with_note(format!("{var} would need to contain itself")) + .with_note(format!("recursive shape: {ty}")) + .with_help("add an explicit type annotation or split the recursive call") + } + TypeckDiagnostic::AmbiguousInferredType { span, scheme } => { + Diagnostic::error("ambiguous inferred type") + .with_code(DiagnosticCode::TYPECK_AMBIGUOUS_INFERENCE_OR_TYPE_CONSTRUCTOR_ARITY) + .with_primary_label_span(span.clone(), Some("ambiguous inferred type")) + .with_note(scheme.clone()) + .with_help("add a type annotation or a matching impl to fix the ambiguous type variable") + } + TypeckDiagnostic::TypeConstructorArity { + span, + constructor, + ty, + expected, + actual, + } => Diagnostic::error("Invalid number of type arguments!") + .with_code(DiagnosticCode::TYPECK_AMBIGUOUS_INFERENCE_OR_TYPE_CONSTRUCTOR_ARITY) + .with_primary_label_span(span.clone(), Some("diagnostic reported here")) + .with_note(format!( + "Type {constructor} is expected to have {expected} type arguments" + )) + .with_note(format!("but, type {ty} has {actual} arguments")), + TypeckDiagnostic::UndefinedTypeVariables { vars } => { + let names = vars + .iter() + .map(|(_, name)| name.as_str()) + .collect::>() + .join(" "); + let mut diagnostic = + Diagnostic::error(format!("undefined type variables: {names}")) + .with_code(DiagnosticCode::TYPECK_UNDEFINED_TYPE_VARIABLES); + for (span, _) in vars { + diagnostic = diagnostic + .with_primary_label_span(span.clone(), Some("undefined type variable")); + } + diagnostic + } + TypeckDiagnostic::WrongArity { + span, + context, + expected, + actual, + callee, + } => { + let expected_noun = plural(*expected, "argument", "arguments"); + let actual_noun = plural(*actual, "argument", "arguments"); + let actual_verb = if *actual == 1 { "was" } else { "were" }; + let mut diagnostic = Diagnostic::error(format!( + "{context} expects {expected} {expected_noun}, but {actual} {actual_verb} provided" + )) + .with_code(DiagnosticCode::TYPECK_WRONG_ARITY) + .with_primary_label_span(span.clone(), Some("wrong number of arguments")) + .with_note(format!("expected {expected} {expected_noun}")) + .with_note(format!("found {actual} {actual_noun}")); + if let Some(callee) = callee { + if let Some(definition) = &callee.definition { + diagnostic = diagnostic.with_secondary_label_span( + definition.clone(), + Some(format!("`{}` defined here", callee.name)), + ); + } + diagnostic = diagnostic.with_note(format!( + "`{}` has signature `{}`", + callee.name, callee.signature + )); + } + diagnostic + } + TypeckDiagnostic::MutualRecursiveData { span, ty } => { + Diagnostic::error(format!("undefined type: {ty}")) + .with_code(DiagnosticCode::TYPECK_MUTUAL_RECURSIVE_DATA) + .with_primary_label_span(span.clone(), Some("undefined type")) + } + TypeckDiagnostic::NonWordYulVar { span, name, actual } => Diagnostic::error(format!( + "Yul reference `{name}` requires word type, got {actual}" + )) + .with_code(DiagnosticCode::TYPECK_NON_WORD_YUL_VAR) + .with_primary_label_span(span.clone(), Some("Yul reference has non-word type")), + TypeckDiagnostic::UnknownField { span, field } => { + Diagnostic::error(format!("cannot resolve field `{field}`")) + .with_code(DiagnosticCode::TYPECK_UNKNOWN_FIELD) + .with_primary_label_span(span.clone(), Some("unknown field")) + .with_help("check that the receiver has this field or constructor path") + } + TypeckDiagnostic::NonCallable { span, callee } => { + Diagnostic::error(format!("non-callable value of type {callee}")) + .with_code(DiagnosticCode::TYPECK_NON_CALLABLE) + .with_primary_label_span(span.clone(), Some("callee is not callable")) + } + TypeckDiagnostic::NamespaceAsValue { + span, + name, + namespace, + position, + } => { + let subject = match namespace { + ValueNamespace::Type => "type name", + ValueNamespace::Class => "trait name", + ValueNamespace::Module => "module", + ValueNamespace::TypeVariable => "type variable", + }; + let message = match position { + ValuePosition::Value => format!("{subject} used as value: `{name}`"), + ValuePosition::Callee => format!("{subject} used as callee: `{name}`"), + }; + Diagnostic::error(message) + .with_code(DiagnosticCode::TYPECK_NAMESPACE_AS_VALUE) + .with_primary_label_span(span.clone(), Some("not a value")) + .with_help("use a constructor or value binding here, not a namespace name") + } + TypeckDiagnostic::ClassAsType { span, class } => { + Diagnostic::error(format!("trait name used as type: `{class}`")) + .with_code(DiagnosticCode::TYPECK_CLASS_AS_TYPE) + .with_primary_label_span(span.clone(), Some("trait is not a type")) + } + TypeckDiagnostic::DuplicateType { + span, + name, + previous, + } => { + let diagnostic = Diagnostic::error(format!("duplicate type definition: {name}")) + .with_code(DiagnosticCode::TYPECK_DUPLICATE_TYPE) + .with_primary_label_span(span.clone(), Some("duplicate type")); + let diagnostic = if let Some(previous) = previous { + diagnostic.with_secondary_label_span( + previous.clone(), + Some("existing definition"), + ) + } else { + diagnostic.with_note(format!("existing definition: enum {name}")) + }; + diagnostic.with_note("rename or remove the duplicate type definition") + } + TypeckDiagnostic::UnsatisfiedConstraint { span, pred } => { + Diagnostic::error(format!("cannot satisfy trait constraint: {pred}")) + .with_code(DiagnosticCode::TYPECK_UNSATISFIED_CONSTRAINT) + .with_primary_label_span(span.clone(), Some("constraint originates here")) + .with_note(format!("no visible impl matches `{pred}`")) + .with_help("add a matching impl or strengthen the surrounding type context") + } + TypeckDiagnostic::AmbiguousConstraint { + span, + pred, + candidates, + } => { + let mut diagnostic = + Diagnostic::error(format!("ambiguous trait constraint: {pred}")) + .with_code(DiagnosticCode::TYPECK_AMBIGUOUS_CONSTRAINT) + .with_primary_label_span(span.clone(), Some("ambiguous constraint here")) + .with_help("make the type more specific or remove overlapping impls"); + for candidate in candidates { + diagnostic = diagnostic.with_note(candidate.clone()); + } + diagnostic + } + TypeckDiagnostic::SolverFuelExhausted { span, pred } => Diagnostic::error(format!( + "cannot solve trait constraint `{pred}`: solver exceeded its iteration bound" + )) + .with_code(DiagnosticCode::TYPECK_SOLVER_FUEL_EXHAUSTED) + .with_primary_label_span(span.clone(), Some("constraint originates here")) + .with_help("simplify the impl chain or add a more direct impl"), + TypeckDiagnostic::NonFinalReturn { span } => { + Diagnostic::error("illegal return statement") + .with_code(DiagnosticCode::TYPECK_NON_FINAL_RETURN_OR_INVALID_CONSTRUCTOR_PATTERN) + .with_primary_label_span(span.clone(), Some("return before end of block")) + .with_note("return statements must be the final statement in a block") + } + TypeckDiagnostic::UnknownYulName { span, name } => { + Diagnostic::error(format!("unknown Yul identifier or function: {name}")) + .with_code(DiagnosticCode::TYPECK_UNKNOWN_YUL_NAME) + .with_primary_label_span(span.clone(), Some("unknown Yul name")) + } + TypeckDiagnostic::CoverageCondition { + span, + class, + main, + undetermined, + } => Diagnostic::error(format!( + "Coverage condition fails for trait:\n{class}\n- the type:\n{main}\ndoes not determine:\n{}", + undetermined.join(", ") + )) + .with_code(DiagnosticCode::TYPECK_COVERAGE_CONDITION) + .with_primary_label_span(span.clone(), Some("impl head does not determine these variables")), + TypeckDiagnostic::PattersonCondition { span, head } => Diagnostic::error(format!( + "impl `{head}` does not satisfy the Patterson conditions" + )) + .with_code(DiagnosticCode::TYPECK_PATTERSON_CONDITION) + .with_primary_label_span(span.clone(), Some("impl head violates Patterson condition")) + .with_note("each impl constraint must be structurally smaller than the impl head") + .with_help("remove the recursive constraint, add a more specific impl, or use the Patterson-condition pragma intentionally"), + TypeckDiagnostic::BoundedVariableCondition { span } => { + Diagnostic::error("Bounded variable condition fails!") + .with_code(DiagnosticCode::TYPECK_BOUNDED_VARIABLE_CONDITION) + .with_primary_label_span(span.clone(), Some("impl head is missing constraint variables")) + } + TypeckDiagnostic::TypeAliasCycle { span, alias } => { + Diagnostic::error(format!("recursive type alias `{alias}`")) + .with_code(DiagnosticCode::TYPECK_TYPE_ALIAS_CYCLE) + .with_primary_label_span(span.clone(), Some("recursive alias")) + } + TypeckDiagnostic::TypeAliasArity { + span, + alias, + expected, + actual, + } => Diagnostic::error(format!( + "type synonym arity mismatch for `{alias}`: expected {expected}, got {actual}" + )) + .with_code(DiagnosticCode::TYPECK_TYPE_ALIAS_ARITY) + .with_primary_label_span(span.clone(), Some("type alias arity mismatch")), + TypeckDiagnostic::TypeAliasExpansionLimit { span, limit } => Diagnostic::error( + format!("type synonym expansion exceeded {limit} type nodes"), + ) + .with_code(DiagnosticCode::TYPECK_TYPE_ALIAS_EXPANSION_LIMIT) + .with_primary_label_span(span.clone(), Some("type alias expansion starts here")), + TypeckDiagnostic::ClassArity { + span, + class, + expected, + actual, + } => Diagnostic::error(format!( + "trait arity mismatch for `{class}`: expected {expected}, got {actual}" + )) + .with_code(DiagnosticCode::TYPECK_CLASS_ARITY) + .with_primary_label_span(span.clone(), Some("trait constraint arity mismatch")), + TypeckDiagnostic::OverlappingInstance { + instance_span, + overlaps_span, + instance, + overlaps, + } => { + let diagnostic = Diagnostic::error(format!( + "Overlapping impls are not supported\nimpl:\n{instance}\noverlaps with:\n{overlaps}" + )) + .with_code(DiagnosticCode::TYPECK_OVERLAPPING_INSTANCE) + .with_primary_label_span(instance_span.clone(), Some("overlapping impl")); + if let Some(overlaps_span) = overlaps_span { + diagnostic.with_secondary_label_span( + overlaps_span.clone(), + Some("previous overlapping impl"), + ) + } else { + diagnostic + } + } + TypeckDiagnostic::InvalidDefaultInstance { span, head } => Diagnostic::error(format!( + "Cannot have a default impl whose main argument contains no type variable: {head}" + )) + .with_code(DiagnosticCode::TYPECK_INVALID_DEFAULT_INSTANCE) + .with_primary_label_span(span.clone(), Some("invalid default impl head")), + TypeckDiagnostic::IncompleteInstance { + span, + class, + missing, + } => Diagnostic::error(format!( + "Incomplete impl for trait:\n{class}\nmissing definitions for:\n{}", + missing.join(", ") + )) + .with_code(DiagnosticCode::TYPECK_INCOMPLETE_INSTANCE) + .with_primary_label_span(span.clone(), Some("incomplete impl")), + TypeckDiagnostic::UnknownInstanceMethod { + span, + name, + class_span, + } => { + let diagnostic = Diagnostic::error(format!("undefined name: {name}")) + .with_code(DiagnosticCode::TYPECK_RECURSIVE_TYPE_OR_UNKNOWN_INSTANCE_METHOD) + .with_primary_label_span(span.clone(), Some("unknown name")); + if let Some(class_span) = class_span { + diagnostic.with_secondary_label_span( + class_span.clone(), + Some("trait defined here"), + ) + } else { + diagnostic + } + } + TypeckDiagnostic::IncompleteSignature { span, signature } => Diagnostic::error( + "top-level function must have complete type annotations", + ) + .with_code(DiagnosticCode::TYPECK_INCOMPLETE_SIGNATURE) + .with_primary_label_span(span.clone(), Some("incomplete signature")) + .with_note(format!("signature: {signature}")) + .with_note("annotate every parameter (`name: Type`); omit `returns` for a function that returns no value"), + TypeckDiagnostic::IncompleteMethodSignature { span, signature } => Diagnostic::error( + "trait and impl methods must have complete type signatures", + ) + .with_code(DiagnosticCode::TYPECK_INCOMPLETE_METHOD_SIGNATURE) + .with_primary_label_span(span.clone(), Some("incomplete method signature")) + .with_note(format!("signature: {signature}")) + .with_note("annotate every method parameter; omit `returns` for a method that returns no value"), + TypeckDiagnostic::InvalidInstanceMethodSignature { + span, + method, + reason, + } => { + Diagnostic::error(format!( + "invalid impl member signature for `{method}`: {reason}" + )) + .with_code(DiagnosticCode::TYPECK_INVALID_INSTANCE_METHOD_SIGNATURE) + .with_primary_label_span(span.clone(), Some("invalid impl method signature")) + .with_note("the impl method must match the trait method after substituting the impl head") + } + TypeckDiagnostic::InvalidConstructorPattern { span, name } => Diagnostic::error(format!( + "constructor pattern `{name}` does not resolve to a constructor" + )) + .with_code(DiagnosticCode::TYPECK_NON_FINAL_RETURN_OR_INVALID_CONSTRUCTOR_PATTERN) + .with_primary_label_span(span.clone(), Some("invalid constructor pattern")), + TypeckDiagnostic::HiddenConstructorCoverage { span, ty } => Diagnostic::error(format!( + "pattern match on type with hidden constructors requires a wildcard arm: {ty}" + )) + .with_code(DiagnosticCode::TYPECK_HIDDEN_CONSTRUCTOR_COVERAGE) + .with_primary_label_span(span.clone(), Some("match needs a wildcard arm")), + TypeckDiagnostic::ShorthandConstructor { span, name, reason } => Diagnostic::error(format!( + "cannot resolve shorthand constructor `.{name}`: {reason}" + )) + .with_code(DiagnosticCode::TYPECK_SHORTHAND_CONSTRUCTOR) + .with_primary_label_span(span.clone(), Some("shorthand constructor")), + TypeckDiagnostic::GenericDeriveConflict { span, ty } => Diagnostic::error(format!( + "type '{ty}' has a manual Generic impl but no 'pragma solcore noGenericInstanceFor {ty};'; add the pragma to suppress auto-derivation" + )) + .with_code(DiagnosticCode::TYPECK_GENERIC_DERIVE_CONFLICT) + .with_primary_label_span(span.clone(), Some("manual Generic impl conflicts with auto-derivation")), + TypeckDiagnostic::RuntimeToComptimeParam { + span, + function, + param, + } => { + Diagnostic::error(format!( + "runtime value passed to comptime parameter '{param}' of '{function}'" + )) + .with_code(DiagnosticCode::TYPECK_RUNTIME_TO_COMPTIME_PARAM) + .with_primary_label_span(span.clone(), Some("runtime value passed here")) + } + TypeckDiagnostic::ComptimeLetRuntime { span, name } => Diagnostic::error(format!( + "`let comptime {name}` is bound to a runtime expression" + )) + .with_code(DiagnosticCode::TYPECK_COMPTIME_LET_RUNTIME) + .with_primary_label_span(span.clone(), Some("runtime initializer")), + TypeckDiagnostic::ComptimeReturnRuntime { span, context } => Diagnostic::error(format!( + "{context}: function with a comptime return type returns a runtime expression" + )) + .with_code(DiagnosticCode::TYPECK_COMPTIME_RETURN_RUNTIME) + .with_primary_label_span(span.clone(), Some("runtime return expression")), + TypeckDiagnostic::InvalidConversion { + target_span, + operand_span, + source, + target, + } => Diagnostic::error(format!("cannot convert `{source}` to `{target}`")) + .with_code(DiagnosticCode::TYPECK_INVALID_CONVERSION) + .with_primary_label_span( + target_span.clone(), + Some("conversion to this type is not defined"), + ) + .with_secondary_label_span( + operand_span.clone(), + Some(format!("expression has type `{source}`")), + ) + .with_help(format!( + "if you intended to guide inference, use `let value: {target} = expression`" + )), + TypeckDiagnostic::ConversionAssignmentTarget { span } => { + Diagnostic::error("cannot assign through a conversion") + .with_code(DiagnosticCode::TYPECK_INVALID_CONVERSION) + .with_primary_label_span( + span.clone(), + Some("a converted value is not an assignment location"), + ) + .with_help("assign to the original variable, field, or index instead") + } + TypeckDiagnostic::UnsupportedStorageFieldProjection { span, field } => { + Diagnostic::error(format!( + "cannot project field `{field}` from a contract-storage value" + )) + .with_code(DiagnosticCode::TYPECK_UNSUPPORTED_STORAGE_FIELD_PROJECTION) + .with_primary_label_span( + span.clone(), + Some("storage field projection is not supported"), + ) + .with_help( + "access named fields only on local values; contract-storage struct layout is not implemented", + ) + } + TypeckDiagnostic::InvalidValueTypeDefinition { span, name, reason } => { + Diagnostic::error(format!( + "invalid user-defined value type `{name}`: {reason}" + )) + .with_code(DiagnosticCode::TYPECK_INVALID_VALUE_TYPE_DEFINITION) + .with_primary_label_span( + span.clone(), + Some("invalid user-defined value type"), + ) + .with_note( + "value types are nominal, non-generic wrappers over one Solidity elementary value type", + ) + } + TypeckDiagnostic::UnsupportedValueTypeStorage { span, ty } => { + Diagnostic::error(format!( + "user-defined value type `{ty}` cannot be stored by this backend" + )) + .with_code(DiagnosticCode::TYPECK_UNSUPPORTED_VALUE_TYPE_STORAGE) + .with_primary_label_span( + span.clone(), + Some("unsupported value-type storage representation"), + ) + .with_note( + "contract storage currently supports only value types with a one-word representation", + ) + } + TypeckDiagnostic::UnsupportedFixedArrayStorage { span, ty } => { + Diagnostic::error(format!( + "fixed-length array `{ty}` cannot be stored by this backend" + )) + .with_code(DiagnosticCode::TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE) + .with_primary_label_span( + span.clone(), + Some("fixed-length array storage is unsupported"), + ) + .with_note( + "fixed-length arrays require a dedicated contract-storage layout and load/store lowering", + ) + } + TypeckDiagnostic::NonExhaustiveMatch { span, missing } => { + Diagnostic::error("non-exhaustive pattern match") + .with_code(DiagnosticCode::TYPECK_NON_EXHAUSTIVE_MATCH) + .with_primary_label_span(span.clone(), Some("non-exhaustive match")) + .with_note(format!("missing case: {missing}")) + .with_note("help: add a clause that covers the missing case") + } + TypeckDiagnostic::UnreachableMatchArm { span } => { + Diagnostic::warning("unreachable match arm") + .with_code(DiagnosticCode::TYPECK_UNREACHABLE_MATCH_ARM) + .with_primary_label_span(span.clone(), Some("this arm is unreachable")) + .with_note("this arm is covered by previous match arms") + } + } + } +} + +pub(super) fn alias_error_to_diagnostic(error: AliasError) -> TypeckDiagnostic { + match error { + AliasError::Cycle { span, alias } => TypeckDiagnostic::TypeAliasCycle { span, alias }, + AliasError::Arity { + span, + alias, + expected, + actual, + } => TypeckDiagnostic::TypeAliasArity { + span, + alias, + expected, + actual, + }, + AliasError::ExpansionLimit { span, limit } => { + TypeckDiagnostic::TypeAliasExpansionLimit { span, limit } + } + } +} + +fn plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str { + if count == 1 { singular } else { plural } +} + +fn parameter_display(param: &ParameterDiagnostic) -> String { + param + .name + .as_ref() + .map(|name| format!("parameter `{name}`")) + .unwrap_or_else(|| format!("parameter {}", param.index + 1)) +} + +fn parameter_definition_label( + param: &ParameterDiagnostic, + callee: Option<&CalleeDiagnostic>, +) -> String { + if param.definition.is_some() { + return param + .name + .as_ref() + .map(|name| format!("parameter `{name}` defined here")) + .unwrap_or_else(|| format!("parameter {} defined here", param.index + 1)); + } + callee + .map(|callee| format!("`{}` defined here", callee.name)) + .unwrap_or_else(|| "parameter defined here".to_owned()) +} + +pub(super) fn callee_diagnostic_info<'db>( + db: &'db dyn Db, + entry: Option>, + callee: &CallSiteCallee<'db>, +) -> Option { + match callee { + CallSiteCallee::Function(def) => { + let module = def_hir_module(db, *def); + let info = find_function_info(db, module, *def)?; + let name = ident_text(db, &info.function.sig(db).name); + let names = function_param_names(db, info.function.sig(db)); + let type_var_names = type_var_names(db, &info.type_vars); + let scheme = function_callee_scheme(db, entry, module, *def)?; + let signature = source_signature_from_func_sig(db, &name, info.function.sig(db)) + .unwrap_or_else(|| { + signature_from_scheme(db, &name, &names, &type_var_names, scheme) + }); + Some(CalleeDiagnostic { + name: name.clone(), + signature, + definition: def_name_label_span(db, *def), + }) + } + CallSiteCallee::AdtCtor { ty, index } => { + let module = def_hir_module(db, *ty); + let info = find_adt_info(db, module, *ty)?; + let ctor = info.adt.ctors(db).get(index.as_usize())?; + let name = ident_text(db, &ctor.name); + let type_var_names = type_var_names(db, &info.type_vars); + let scheme = adt_ctor_callee_scheme(db, entry, module, *ty, *index)?; + let source_params = adt_ctor_source_params(db, ctor); + Some(CalleeDiagnostic { + name: name.clone(), + signature: signature_from_scheme_with_source_params( + db, + &name, + &type_var_names, + scheme, + &source_params, + ), + definition: Some(LabelSpan::from_span(db, ctor.name.span(db))), + }) + } + CallSiteCallee::ClassMethod { class, name } => { + let module = def_hir_module(db, *class); + let info = find_class_info(db, module, *class)?; + let method = info + .class + .methods(db) + .iter() + .find(|method| ident_text(db, &method.name) == name.as_str())?; + let param_names = function_param_names(db, method); + let method_type_vars = class_method_type_vars(db, info.class, method); + let type_var_names = type_var_names(db, &method_type_vars); + let scheme = class_method_callee_scheme(db, entry, module, *class, name.clone())?; + let signature = source_signature_from_func_sig(db, name, method).unwrap_or_else(|| { + signature_from_scheme(db, name, ¶m_names, &type_var_names, scheme) + }); + Some(CalleeDiagnostic { + name: name.clone(), + signature, + definition: Some(LabelSpan::from_span(db, method.name.span(db))), + }) + } + CallSiteCallee::Builtin(kind) => { + let name = builtin_name(*kind)?.to_owned(); + let scheme = builtin_scheme(db, *kind)?; + Some(CalleeDiagnostic { + signature: signature_from_scheme(db, &name, &[], &[], scheme), + name, + definition: None, + }) + } + CallSiteCallee::Closure(_) | CallSiteCallee::Invokable | CallSiteCallee::Field(_) => None, + } +} + +pub(super) fn call_param_diagnostic_info<'db>( + db: &'db dyn Db, + callee: Option<&CallSiteCallee<'db>>, + index: usize, +) -> ParameterDiagnostic { + let Some(callee) = callee else { + return ParameterDiagnostic { + index, + name: None, + ty: None, + definition: None, + }; + }; + match callee { + CallSiteCallee::Function(def) => { + let module = def_hir_module(db, *def); + let param = find_function_info(db, module, *def) + .and_then(|info| info.function.sig(db).params.atom().get(index).cloned()); + parameter_from_func_param(db, index, param.as_ref()) + } + CallSiteCallee::ClassMethod { class, name } => { + let module = def_hir_module(db, *class); + let param = find_class_info(db, module, *class).and_then(|info| { + info.class + .methods(db) + .iter() + .find(|method| ident_text(db, &method.name) == name.as_str()) + .and_then(|method| method.params.atom().get(index).cloned()) + }); + parameter_from_func_param(db, index, param.as_ref()) + } + CallSiteCallee::AdtCtor { ty, index: ctor } => { + let module = def_hir_module(db, *ty); + let ctor = find_adt_info(db, module, *ty) + .and_then(|info| info.adt.ctors(db).get(ctor.as_usize()).cloned()); + let ty = ctor + .as_ref() + .and_then(|ctor| adt_ctor_source_params(db, ctor).get(index).copied()) + .map(|ty| display_type_ref_source(db, ty)); + let definition = ctor + .as_ref() + .and_then(|ctor| ctor_param_label_span(db, ctor, index)); + ParameterDiagnostic { + index, + name: None, + ty, + definition, + } + } + CallSiteCallee::Builtin(_) + | CallSiteCallee::Closure(_) + | CallSiteCallee::Invokable + | CallSiteCallee::Field(_) => ParameterDiagnostic { + index, + name: None, + ty: None, + definition: None, + }, + } +} + +pub(super) fn def_name_label_span<'db>(db: &'db dyn Db, def: DefId<'db>) -> Option { + let module = def_hir_module(db, def); + find_def_name_span_in_module(db, module, def).map(|span| LabelSpan::from_span(db, span)) +} + +fn function_callee_scheme<'db>( + db: &'db dyn Db, + entry: Option>, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + entry + .and_then(|entry| function_scheme_for_entry(db, entry, def)) + .or_else(|| function_scheme_in_hir_module(db, module, def)) +} + +fn adt_ctor_callee_scheme<'db>( + db: &'db dyn Db, + entry: Option>, + module: Module<'db>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, +) -> Option> { + entry + .and_then(|entry| adt_ctor_scheme_for_entry(db, entry, ty, index)) + .or_else(|| adt_ctor_scheme_in_hir_module(db, module, ty, index)) +} + +fn class_method_callee_scheme<'db>( + db: &'db dyn Db, + entry: Option>, + module: Module<'db>, + class: DefId<'db>, + name: String, +) -> Option> { + entry + .and_then(|entry| class_method_scheme_for_entry(db, entry, class, name.clone())) + .or_else(|| class_method_scheme_in_hir_module(db, module, class, name)) +} + +fn signature_from_scheme<'db>( + db: &'db dyn Db, + name: &str, + param_names: &[String], + type_var_names: &[String], + scheme: TyScheme<'db>, +) -> String { + let ty = scheme.body(db).ty(db); + let (params, ret) = match ty.kind(db) { + TyKind::Function { params, ret } => (params.clone(), *ret), + _ => (Vec::new(), ty), + }; + let parameters = params + .iter() + .enumerate() + .map(|(index, param)| { + let ty = display_ty_source(db, *param, type_var_names); + param_names + .get(index) + .map(|name| format!("{name}: {ty}")) + .unwrap_or(ty) + }) + .collect::>(); + format!( + "{name}({}){}", + parameters.join(", "), + display_ty_return_suffix(db, ret, type_var_names) + ) +} + +fn signature_from_scheme_with_source_params<'db>( + db: &'db dyn Db, + name: &str, + type_var_names: &[String], + scheme: TyScheme<'db>, + source_params: &[TypeRef<'db>], +) -> String { + let ty = scheme.body(db).ty(db); + let ret = match ty.kind(db) { + TyKind::Function { ret, .. } => *ret, + _ => ty, + }; + let parameters = source_params + .iter() + .map(|param| display_type_ref_source(db, *param)) + .collect::>(); + format!( + "{name}({}){}", + parameters.join(", "), + display_ty_return_suffix(db, ret, type_var_names) + ) +} + +fn source_signature_from_func_sig<'db>( + db: &'db dyn HirDb, + name: &str, + sig: &FuncSig<'db>, +) -> Option { + let mut params = Vec::new(); + for param in sig.params.atom() { + match param { + FuncParam::Typed { comptime, name, ty } => { + let prefix = if comptime.is_some() { "comptime " } else { "" }; + params.push(format!( + "{prefix}{}: {}", + ident_text(db, name), + display_type_ref_source(db, *ty) + )); + } + FuncParam::Untyped { .. } | FuncParam::Error { .. } => return None, + } + } + let return_suffix = sig + .ret + .map(|ret| display_type_ref_return_suffix(db, ret)) + .unwrap_or_default(); + Some(format!("{name}({}){}", params.join(", "), return_suffix)) +} + +fn def_hir_module<'db>(db: &'db dyn Db, def: DefId<'db>) -> Module<'db> { + parse_file_to_hir(db, def.file(db)).module(db) +} + +fn function_param_names<'db>(db: &'db dyn HirDb, sig: &FuncSig<'db>) -> Vec { + sig.params + .atom() + .iter() + .filter_map(|param| match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some(ident_text(db, name)) + } + FuncParam::Error { .. } => None, + }) + .collect() +} + +fn type_var_names<'db>( + db: &'db dyn HirDb, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> Vec { + let mut names = Vec::new(); + for var in type_vars { + let index = var.index as usize; + if names.len() <= index { + names.resize(index + 1, "_".to_owned()); + } + names[index] = ident_text(db, &var.name); + } + names +} + +fn parameter_from_func_param<'db>( + db: &'db dyn HirDb, + index: usize, + param: Option<&FuncParam<'db>>, +) -> ParameterDiagnostic { + match param { + Some(FuncParam::Typed { name, ty, .. }) => ParameterDiagnostic { + index, + name: Some(ident_text(db, name)), + ty: Some(display_type_ref_source(db, *ty)), + definition: Some(LabelSpan::from_span(db, name.span(db))), + }, + Some(FuncParam::Untyped { name, .. }) => ParameterDiagnostic { + index, + name: Some(ident_text(db, name)), + ty: None, + definition: Some(LabelSpan::from_span(db, name.span(db))), + }, + Some(FuncParam::Error { span }) => ParameterDiagnostic { + index, + name: None, + ty: None, + definition: Some(LabelSpan::from_span(db, *span)), + }, + None => ParameterDiagnostic { + index, + name: None, + ty: None, + definition: None, + }, + } +} + +fn ctor_param_label_span<'db>( + db: &'db dyn HirDb, + ctor: &AdtCtor<'db>, + index: usize, +) -> Option { + match ctor.fields.atom().kind(db) { + TypeRefKind::Tuple { elems } => elems + .atom() + .get(index) + .map(|ty| LabelSpan::from_span(db, ty.span(db))), + _ if index == 0 => Some(LabelSpan::from_span(db, ctor.fields.atom().span(db))), + _ => None, + } +} + +fn adt_ctor_source_params<'db>(db: &'db dyn HirDb, ctor: &AdtCtor<'db>) -> Vec> { + let fields = *ctor.fields.atom(); + match ctor.field_count { + 0 => Vec::new(), + 1 => vec![fields], + _ => match fields.kind(db) { + TypeRefKind::Tuple { elems } => elems.atom().clone(), + _ => vec![fields], + }, + } +} + +fn find_def_name_span_in_module<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + Item::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + Item::ClassDef(class) if class.def_id_value(db) == def => { + return Some(class.head(db).kind(db).class.span(db)); + } + Item::InstanceDef(instance) if instance.def_id_value(db) == def => { + return Some(instance.head(db).span(db)); + } + Item::ContractDef(contract) => { + if contract.def_id_value(db) == def { + return Some(contract.name_elem(db).span(db)); + } + if let Some(span) = find_def_name_span_in_contract(db, contract, def) { + return Some(span); + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + None +} + +fn find_def_name_span_in_contract<'db>( + db: &'db dyn HirDb, + contract: ContractDef<'db>, + def: DefId<'db>, +) -> Option> { + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + ContractItem::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + None +} + +fn builtin_name(kind: hir_nameres::BuiltinKind) -> Option<&'static str> { + Some(match kind { + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::True) => "true", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::False) => "false", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Unit) => "()", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Pair) => "pair", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Inl) => "inl", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Inr) => "inr", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::Invoke) => "invoke", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimAddWord) => { + "primAddWord" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimEqWord) => { + "primEqWord" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordToInteger) => { + "wordToInteger" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordFromInteger) => { + "wordFromInteger" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerAdd) => { + "integerAdd" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerSub) => { + "integerSub" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerMul) => { + "integerMul" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerLt) => "integerLt", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerEq) => "integerEq", + hir_nameres::BuiltinKind::ClassMethod(hir_nameres::BuiltinClassMethod::InvokableInvoke) => { + "invoke" + } + hir_nameres::BuiltinKind::ClassMethod(hir_nameres::BuiltinClassMethod::IntFromInteger) => { + "fromInteger" + } + hir_nameres::BuiltinKind::Type(_) | hir_nameres::BuiltinKind::Class(_) => return None, + }) +} + +pub(super) fn lowering_diagnostic_to_typeck( + diagnostic: TypeLoweringDiagnostic, +) -> TypeckDiagnostic { + match diagnostic { + TypeLoweringDiagnostic::ClassAsType { span, class } => { + TypeckDiagnostic::ClassAsType { span, class } + } + } +} + +pub(super) fn item_type_constructor_arity_diagnostics<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + resolutions: &hir_nameres::ItemResolutionFacts<'db>, +) -> Vec { + resolutions + .types + .iter() + .filter_map(|resolution| { + type_constructor_arity_diagnostic( + db, + entry, + resolution.ty, + &resolution.resolution, + None, + ) + }) + .collect() +} + +pub(super) fn body_type_constructor_arity_diagnostics<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + body: FuncBody<'db>, + resolutions: &hir_nameres::BodyResolutionMap<'db>, + pre_typeck_desugar: &[BodyPreTypeckDesugarPlan<'db>], +) -> Vec { + let mut skip = FxHashSet::default(); + collect_uninitialized_let_type_refs(db, body, &mut skip); + let sources = DiagnosticSourceMap::new(db, pre_typeck_desugar); + resolutions + .types + .iter() + .filter(|resolution| !skip.contains(&resolution.ty)) + .filter_map(|resolution| { + type_constructor_arity_diagnostic( + db, + entry, + resolution.ty, + &resolution.resolution, + Some(sources.type_label_span(resolution.ty)), + ) + }) + .collect() +} + +fn collect_uninitialized_let_type_refs<'db>( + db: &'db dyn HirDb, + body: FuncBody<'db>, + out: &mut FxHashSet>, +) { + for stmt in body.top_level_stmts(db) { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } +} + +fn collect_uninitialized_let_type_refs_from_stmt<'db>( + db: &'db dyn HirDb, + body: FuncBody<'db>, + stmt: Id>, + out: &mut FxHashSet>, +) { + match &body.stmts(db).get(stmt).kind { + StmtKind::Let { + ty: Some(ty), + init: None, + .. + } => { + collect_type_ref_tree(db, *ty, out); + } + StmtKind::Let { init, .. } => { + if let Some(init) = init { + collect_uninitialized_let_type_refs_from_expr(db, body, *init, out); + } + } + StmtKind::Return(expr) => { + if let Some(expr) = expr { + collect_uninitialized_let_type_refs_from_expr(db, body, *expr, out); + } + } + StmtKind::Expr(expr) => { + collect_uninitialized_let_type_refs_from_expr(db, body, *expr, out); + } + StmtKind::Assign { lhs, rhs, .. } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *lhs, out); + collect_uninitialized_let_type_refs_from_expr(db, body, *rhs, out); + } + StmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + collect_uninitialized_let_type_refs_from_expr(db, body, *scrutinee, out); + } + for arm in arms { + for stmt in &arm.body { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + } + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *cond, out); + for stmt in then_body { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + if let Some(else_body) = else_body { + for stmt in else_body { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + } + } + StmtKind::For { + init, + cond, + post, + body: for_body, + } => { + for stmt in init { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + collect_uninitialized_let_type_refs_from_expr(db, body, *cond, out); + for stmt in post { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + for stmt in for_body { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + } + StmtKind::Block { body: block } => { + for stmt in block { + collect_uninitialized_let_type_refs_from_stmt(db, body, *stmt, out); + } + } + StmtKind::Assembly { .. } | StmtKind::Break | StmtKind::Continue | StmtKind::Error => {} + } +} + +fn collect_uninitialized_let_type_refs_from_expr<'db>( + db: &'db dyn HirDb, + body: FuncBody<'db>, + expr: Id>, + out: &mut FxHashSet>, +) { + match &body.exprs(db).get(expr).kind { + ExprKind::Lambda { + params: _, + ret: _, + body: lambda_body, + } => { + collect_uninitialized_let_type_refs(db, *lambda_body, out); + } + ExprKind::Tuple(exprs) | ExprKind::DotCtor { args: exprs, .. } => { + for expr in exprs { + collect_uninitialized_let_type_refs_from_expr(db, body, *expr, out); + } + } + ExprKind::BinOp { lhs, rhs, .. } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *lhs, out); + collect_uninitialized_let_type_refs_from_expr(db, body, *rhs, out); + } + ExprKind::UnaryOp { expr, .. } + | ExprKind::Conversion { expr, .. } + | ExprKind::TypeAscription { expr, .. } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *expr, out); + } + ExprKind::Call { callee, args } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *callee, out); + for arg in args { + collect_uninitialized_let_type_refs_from_expr(db, body, *arg, out); + } + } + ExprKind::Field { base, .. } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *base, out); + } + ExprKind::Index { base, index } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *base, out); + collect_uninitialized_let_type_refs_from_expr(db, body, *index, out); + } + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + collect_uninitialized_let_type_refs_from_expr(db, body, *cond, out); + collect_uninitialized_let_type_refs_from_expr(db, body, *then_expr, out); + collect_uninitialized_let_type_refs_from_expr(db, body, *else_expr, out); + } + ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::Proxy { .. } | ExprKind::Error => {} + } +} + +fn collect_type_ref_tree<'db>( + db: &'db dyn HirDb, + ty: TypeRef<'db>, + out: &mut FxHashSet>, +) { + if !out.insert(ty) { + return; + } + match ty.kind(db) { + TypeRefKind::Named { args, .. } => { + for arg in args.atom() { + collect_type_ref_tree(db, *arg, out); + } + } + TypeRefKind::FixedArray { element, .. } => collect_type_ref_tree(db, *element, out), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + collect_type_ref_tree(db, *param, out); + } + collect_type_ref_tree(db, *ret, out); + } + TypeRefKind::Comptime { inner, .. } => collect_type_ref_tree(db, *inner, out), + TypeRefKind::Tuple { elems } => { + for elem in elems.atom() { + collect_type_ref_tree(db, *elem, out); + } + } + TypeRefKind::Error { .. } => {} + } +} + +fn type_constructor_arity_diagnostic<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + ty: TypeRef<'db>, + resolution: &hir_nameres::Resolution<'db>, + span: Option, +) -> Option { + let TypeRefKind::Named { args, .. } = ty.kind(db) else { + return None; + }; + let expected = type_constructor_expected_arity(db, entry, resolution)?; + let actual = args.atom().len(); + if expected == actual { + return None; + } + Some(TypeckDiagnostic::TypeConstructorArity { + span: span.unwrap_or_else(|| LabelSpan::from_span(db, ty.span(db))), + constructor: type_ref_constructor_name(db, ty), + ty: format_type_ref(db, ty), + expected, + actual, + }) +} + +fn type_constructor_expected_arity<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + resolution: &hir_nameres::Resolution<'db>, +) -> Option { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Type(ty)) => { + builtin_type_expected_arity(*ty) + } + hir_nameres::Resolution::Def { def, kind } => { + user_type_expected_arity(db, entry, *def, *kind) + } + _ => None, + } +} + +fn builtin_type_expected_arity(ty: hir_nameres::BuiltinType) -> Option { + match ty { + hir_nameres::BuiltinType::Word + | hir_nameres::BuiltinType::Bool + | hir_nameres::BuiltinType::String + | hir_nameres::BuiltinType::Unit + | hir_nameres::BuiltinType::Integer => Some(0), + // The reference `kindCheck` explicitly exempts `pair`. + hir_nameres::BuiltinType::Pair => None, + hir_nameres::BuiltinType::Sum => Some(2), + } +} + +fn user_type_expected_arity<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + def: DefId<'db>, + kind: hir_nameres::DefResolutionKind, +) -> Option { + let module = module_hir(db, module_for_def(db, entry, def)?)?; + match kind { + hir_nameres::DefResolutionKind::Adt => { + find_adt_info(db, module, def).map(|info| info.adt.ty_param_elems(db).len()) + } + // Type aliases already have dedicated normalization diagnostics in + // this crate; keep this pass scoped to kind-checking constructors. + hir_nameres::DefResolutionKind::TypeAlias => None, + hir_nameres::DefResolutionKind::ValueType => Some(0), + hir_nameres::DefResolutionKind::Contract => find_contract_arity(db, module, def), + hir_nameres::DefResolutionKind::Function + | hir_nameres::DefResolutionKind::Class + | hir_nameres::DefResolutionKind::Instance => None, + } +} + +fn find_contract_arity<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option { + module.items(db).iter().find_map(|item| { + let Item::ContractDef(contract) = item else { + return None; + }; + (contract.def_id_value(db) == def).then(|| contract.ty_param_elems(db).len()) + }) +} + +fn type_ref_constructor_name<'db>(db: &'db dyn HirDb, ty: TypeRef<'db>) -> String { + match ty.kind(db) { + TypeRefKind::Named { + qualifier, name, .. + } => { + if let Some(qualifier) = qualifier { + format!("{}.{}", ident_text(db, qualifier), ident_text(db, name)) + } else { + ident_text(db, name) + } + } + _ => format_type_ref(db, ty), + } +} + +#[derive(Clone)] +struct DataCycleNode<'db> { + adt: AdtDef<'db>, + name: String, +} + +#[derive(Clone)] +struct DataCycleEdge<'db> { + from: DefId<'db>, + to: DefId<'db>, + span: LabelSpan, + ty: String, +} + +pub(super) fn mutual_data_diagnostics<'db>( + db: &'db dyn Db, + module: Module<'db>, + resolutions: &hir_nameres::ItemResolutionFacts<'db>, +) -> Vec { + let nodes = local_data_cycle_nodes(db, module); + if nodes.len() < 2 { + return Vec::new(); + } + let local_defs = nodes + .iter() + .map(|node| node.adt.def_id_value(db)) + .collect::>(); + let names = nodes + .iter() + .map(|node| (node.adt.def_id_value(db), node.name.clone())) + .collect::>(); + let type_resolutions = resolutions + .types + .iter() + .map(|resolution| (resolution.ty, resolution.resolution.clone())) + .collect::>(); + let mut edges = Vec::new(); + for node in &nodes { + let from = node.adt.def_id_value(db); + for ctor in node.adt.ctors(db) { + collect_data_cycle_edges( + db, + from, + *ctor.fields.atom(), + &type_resolutions, + &local_defs, + &names, + &mut edges, + ); + } + } + if edges.is_empty() { + return Vec::new(); + } + let adjacency = data_cycle_adjacency(&edges); + let mut reported = FxHashSet::default(); + let mut diagnostics = Vec::new(); + for edge in &edges { + if edge.from == edge.to || !data_path_exists(edge.to, edge.from, &adjacency) { + continue; + } + let mut component = local_defs + .iter() + .copied() + .filter(|def| { + data_path_exists(edge.from, *def, &adjacency) + && data_path_exists(*def, edge.from, &adjacency) + }) + .collect::>(); + if component.len() < 2 { + continue; + } + component.sort_by(|lhs, rhs| names[lhs].cmp(&names[rhs])); + let key = component + .iter() + .map(|def| names[def].as_str()) + .collect::>() + .join("\0"); + if !reported.insert(key) { + continue; + } + let component_defs = component.iter().copied().collect::>(); + let Some(chosen) = choose_data_cycle_edge(&edges, &component_defs, &names) else { + continue; + }; + diagnostics.push(TypeckDiagnostic::MutualRecursiveData { + span: chosen.span.clone(), + ty: chosen.ty.clone(), + }); + } + diagnostics +} + +pub(super) fn dispatch_name_collision_diagnostics<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> Vec { + let reserved = dispatch_reserved_type_names(db, module); + if reserved.is_empty() { + return Vec::new(); + } + let mut diagnostics = Vec::new(); + for item in module.items(db) { + collect_dispatch_name_collisions(db, *item, &reserved, &mut diagnostics); + } + diagnostics +} + +fn dispatch_reserved_type_names<'db>( + db: &'db dyn HirDb, + module: Module<'db>, +) -> FxHashMap { + let mut reserved = FxHashMap::default(); + for item in module.items(db) { + let Item::ContractDef(contract) = item else { + continue; + }; + if contract.kind(db) != ContractKind::Contract { + continue; + } + if contract.items(db).iter().any(|item| { + matches!( + item, + ContractItem::FunctionDef(function) + if ident_text(db, &function.sig(db).name) == "main" + ) + }) { + continue; + } + let contract_name = ident_text(db, &contract.name_elem(db)); + for item in contract.items(db) { + let ContractItem::FunctionDef(function) = item else { + continue; + }; + if !matches!(function.kind(db), FuncKind::Function) { + continue; + } + let sig = function.sig(db); + if !sig.is_abi_visible() { + continue; + } + let method_name = ident_text(db, &sig.name); + if method_name == "fallback" { + continue; + } + reserved + .entry(dispatch_name_type_name(&contract_name, &method_name)) + .or_insert_with(|| LabelSpan::from_span(db, sig.name.span(db))); + } + } + reserved +} + +fn collect_dispatch_name_collisions<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + reserved: &FxHashMap, + diagnostics: &mut Vec, +) { + match item { + Item::AdtDef(adt) => { + let name = ident_text(db, &adt.name_elem(db)); + if let Some(previous) = reserved.get(&name) { + diagnostics.push(TypeckDiagnostic::DuplicateType { + span: LabelSpan::from_span(db, adt.name_elem(db).span(db)), + name, + previous: Some(previous.clone()), + }); + } + } + Item::TypeAlias(alias) => { + let name = ident_text(db, &alias.name_elem(db)); + if let Some(previous) = reserved.get(&name) { + diagnostics.push(TypeckDiagnostic::DuplicateType { + span: LabelSpan::from_span(db, alias.name_elem(db).span(db)), + name, + previous: Some(previous.clone()), + }); + } + } + Item::ContractDef(contract) => { + let name = ident_text(db, &contract.name_elem(db)); + if let Some(previous) = reserved.get(&name) { + diagnostics.push(TypeckDiagnostic::DuplicateType { + span: LabelSpan::from_span(db, contract.name_elem(db).span(db)), + name, + previous: Some(previous.clone()), + }); + } + for item in contract.items(db) { + match *item { + ContractItem::AdtDef(adt) => collect_dispatch_name_collisions( + db, + Item::AdtDef(adt), + reserved, + diagnostics, + ), + ContractItem::TypeAlias(alias) => collect_dispatch_name_collisions( + db, + Item::TypeAlias(alias), + reserved, + diagnostics, + ), + ContractItem::FunctionDef(_) | ContractItem::Error { .. } => {} + } + } + } + Item::ClassDef(class) => { + let class_name = &class.head(db).kind(db).class; + let name = ident_text(db, class_name); + if let Some(previous) = reserved.get(&name) { + diagnostics.push(TypeckDiagnostic::DuplicateType { + span: LabelSpan::from_span(db, class_name.span(db)), + name, + previous: Some(previous.clone()), + }); + } + } + Item::FunctionDef(_) + | Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +fn dispatch_name_type_name(contract: &str, method: &str) -> String { + format!("DispatchNameTy_{contract}_{method}") +} + +fn local_data_cycle_nodes<'db>(db: &'db dyn HirDb, module: Module<'db>) -> Vec> { + let mut nodes = Vec::new(); + for item in module.items(db) { + collect_data_cycle_nodes_from_item(db, *item, &mut nodes); + } + nodes +} + +fn collect_data_cycle_nodes_from_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + nodes: &mut Vec>, +) { + match item { + Item::AdtDef(adt) => nodes.push(DataCycleNode { + adt, + name: ident_text(db, &adt.name_elem(db)), + }), + Item::ContractDef(contract) => { + for item in contract.items(db) { + if let ContractItem::AdtDef(adt) = *item { + collect_data_cycle_nodes_from_item(db, Item::AdtDef(adt), nodes); + } + } + } + _ => {} + } +} + +fn collect_data_cycle_edges<'db>( + db: &'db dyn Db, + from: DefId<'db>, + ty: TypeRef<'db>, + resolutions: &FxHashMap, hir_nameres::Resolution<'db>>, + local_defs: &FxHashSet>, + names: &FxHashMap, String>, + edges: &mut Vec>, +) { + if let Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Adt, + }) = resolutions.get(&ty) + && local_defs.contains(def) + && *def != from + { + edges.push(DataCycleEdge { + from, + to: *def, + span: LabelSpan::from_span(db, ty.span(db)), + ty: names + .get(def) + .cloned() + .unwrap_or_else(|| format_type_ref(db, ty)), + }); + } + match ty.kind(db) { + TypeRefKind::Named { args, .. } => { + for arg in args.atom() { + collect_data_cycle_edges(db, from, *arg, resolutions, local_defs, names, edges); + } + } + TypeRefKind::FixedArray { element, .. } => { + collect_data_cycle_edges(db, from, *element, resolutions, local_defs, names, edges); + } + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + collect_data_cycle_edges(db, from, *param, resolutions, local_defs, names, edges); + } + collect_data_cycle_edges(db, from, *ret, resolutions, local_defs, names, edges); + } + TypeRefKind::Comptime { inner, .. } => { + collect_data_cycle_edges(db, from, *inner, resolutions, local_defs, names, edges); + } + TypeRefKind::Tuple { elems } => { + for elem in elems.atom() { + collect_data_cycle_edges(db, from, *elem, resolutions, local_defs, names, edges); + } + } + TypeRefKind::Error { .. } => {} + } +} + +fn data_cycle_adjacency<'db>( + edges: &[DataCycleEdge<'db>], +) -> FxHashMap, Vec>> { + let mut adjacency = FxHashMap::default(); + for edge in edges { + adjacency + .entry(edge.from) + .or_insert_with(Vec::new) + .push(edge.to); + } + adjacency +} + +fn data_path_exists<'db>( + start: DefId<'db>, + goal: DefId<'db>, + adjacency: &FxHashMap, Vec>>, +) -> bool { + if start == goal { + return true; + } + let mut seen = FxHashSet::default(); + let mut stack = vec![start]; + while let Some(current) = stack.pop() { + if !seen.insert(current) { + continue; + } + let Some(next) = adjacency.get(¤t) else { + continue; + }; + if next.contains(&goal) { + return true; + } + stack.extend(next.iter().copied()); + } + false +} + +fn choose_data_cycle_edge<'db>( + edges: &[DataCycleEdge<'db>], + component: &FxHashSet>, + names: &FxHashMap, String>, +) -> Option> { + let mut candidates = edges + .iter() + .filter(|edge| component.contains(&edge.from) && component.contains(&edge.to)) + .cloned() + .collect::>(); + candidates.sort_by(|lhs, rhs| { + names[&rhs.from] + .cmp(&names[&lhs.from]) + .then_with(|| names[&lhs.to].cmp(&names[&rhs.to])) + }); + candidates.into_iter().next() +} + +pub(super) fn infer_ty_mentions_alias<'db>(ty: &InferTy<'db>) -> bool { + match ty { + InferTy::Named { ctor, args } => { + matches!(ctor, TyCtor::User(user) if matches!(user.kind, UserTyCtorKind::Alias)) + || args.iter().any(infer_ty_mentions_alias) + } + InferTy::Function { params, ret } => { + params.iter().any(infer_ty_mentions_alias) || infer_ty_mentions_alias(ret) + } + InferTy::Tuple(elems) => elems.iter().any(infer_ty_mentions_alias), + InferTy::Comptime(inner) => infer_ty_mentions_alias(inner), + InferTy::Error | InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_) => false, + } +} + +pub(super) fn class_method_resolution<'db>( + resolution: hir_nameres::Resolution<'db>, + expected_method: &str, +) -> Option<(DefId<'db>, String)> { + match resolution { + hir_nameres::Resolution::ClassMethod { class, name } if name == expected_method => { + Some((class, name)) + } + _ => None, + } +} + +pub(super) fn type_ctor_from_resolution<'db>( + resolution: hir_nameres::Resolution<'db>, +) -> Option> { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Type(ty)) => { + let ctor = match ty { + hir_nameres::BuiltinType::Word => BuiltinTyCtor::Word, + hir_nameres::BuiltinType::Bool => BuiltinTyCtor::Bool, + hir_nameres::BuiltinType::String => BuiltinTyCtor::String, + hir_nameres::BuiltinType::Unit => BuiltinTyCtor::Unit, + hir_nameres::BuiltinType::Pair => BuiltinTyCtor::Pair, + hir_nameres::BuiltinType::Sum => BuiltinTyCtor::Sum, + hir_nameres::BuiltinType::Integer => BuiltinTyCtor::Integer, + }; + Some(TyCtor::Builtin(ctor)) + } + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Adt, + } => Some(TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + })), + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::TypeAlias, + } => Some(TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Alias, + })), + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::ValueType, + } => Some(TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::ValueType, + })), + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Contract, + } => Some(TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Contract, + })), + _ => None, + } +} + +pub(super) fn class_id_from_resolution<'db>( + resolution: hir_nameres::Resolution<'db>, +) -> Option> { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Class(class)) => { + let class = match class { + hir_nameres::BuiltinClass::Invokable => BuiltinClassId::Invokable, + hir_nameres::BuiltinClass::Int => BuiltinClassId::Int, + }; + Some(ClassId::Builtin(class)) + } + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } => Some(ClassId::User(def)), + _ => None, + } +} + +pub(super) fn unique_visible_class_method<'db>( + terms: &std::collections::BTreeMap>, + qualified: &str, + expected_method: &str, +) -> Option<(DefId<'db>, String)> { + let suffix = format!(".{qualified}"); + let mut found = None; + for (name, resolution) in terms { + if name != qualified && !name.ends_with(&suffix) { + continue; + } + let Some(candidate) = class_method_resolution(resolution.clone(), expected_method) else { + continue; + }; + if found + .as_ref() + .is_some_and(|existing| existing != &candidate) + { + return None; + } + found = Some(candidate); + } + found +} + +pub(super) fn module_id_for_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> Option> { + let file = module.def_id_value(db).file(db); + let path = hir::url_to_file_path(module.def_id_value(db).file(db).url(db))?; + let tree = db.module_tree(); + let mut candidates = Vec::new(); + if let Some(key) = module_key_for_path(LibraryId::Main, tree.main_root(db), &path) { + candidates.push(module_id_from_key(db, &key)); + } + if let Some(key) = module_key_for_path(LibraryId::Std, tree.std_root(db), &path) { + candidates.push(module_id_from_key(db, &key)); + } + for (name, root) in tree.external_roots(db) { + if let Some(key) = module_key_for_path(LibraryId::External(name.clone()), root, &path) { + candidates.push(module_id_from_key(db, &key)); + } + } + candidates + .iter() + .copied() + .find(|candidate| db.module_file(*candidate) == Some(file)) + .or_else(|| candidates.into_iter().next()) +} + +fn ty_mentions_alias<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Named { ctor, args } => { + matches!(ctor, TyCtor::User(user) if matches!(user.kind, UserTyCtorKind::Alias)) + || args.iter().any(|arg| ty_mentions_alias(db, *arg)) + } + TyKind::Function { params, ret } => { + params.iter().any(|param| ty_mentions_alias(db, *param)) || ty_mentions_alias(db, *ret) + } + TyKind::Tuple(elems) => elems.iter().any(|elem| ty_mentions_alias(db, *elem)), + TyKind::Comptime(inner) => ty_mentions_alias(db, *inner), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + } +} + +pub(super) fn pred_mentions_alias<'db>(db: &'db dyn Db, pred: Pred<'db>) -> bool { + match pred.kind(db) { + PredKind::InClass { main, args, .. } => { + ty_mentions_alias(db, *main) || args.iter().any(|arg| ty_mentions_alias(db, *arg)) + } + PredKind::Eq { lhs, rhs } => ty_mentions_alias(db, *lhs) || ty_mentions_alias(db, *rhs), + PredKind::Error => false, + } +} + +pub(super) fn is_complete_signature(sig: &FuncSig<'_>) -> bool { + sig.params + .atom() + .iter() + .all(|param| matches!(param, FuncParam::Typed { .. })) +} + +pub(super) fn format_func_sig<'db>(db: &'db dyn HirDb, sig: &FuncSig<'db>) -> String { + let mut out = String::new(); + out.push_str("function "); + out.push_str(&ident_text(db, &sig.name)); + if !sig.type_vars.is_empty() { + out.push('<'); + out.push_str( + &sig.type_vars + .iter() + .map(|var| ident_text(db, var)) + .collect::>() + .join(", "), + ); + out.push('>'); + } + out.push('('); + out.push_str( + &sig.params + .atom() + .iter() + .map(|param| format_func_param(db, param)) + .collect::>() + .join(", "), + ); + out.push(')'); + if let Some(visibility) = sig.visibility_kind() { + out.push(' '); + out.push_str(visibility.keyword()); + } + if let Some(mutability) = sig.mutability_kind() { + out.push(' '); + out.push_str(mutability.keyword()); + } + if let Some(ret) = sig.ret { + out.push_str(&display_type_ref_return_suffix(db, ret)); + } + if !sig.preds.is_empty() { + out.push_str(" where "); + out.push_str( + &sig.preds + .iter() + .map(|pred| format_pred_ref(db, *pred)) + .collect::>() + .join(", "), + ); + } + out +} + +fn format_func_param<'db>(db: &'db dyn HirDb, param: &FuncParam<'db>) -> String { + match param { + FuncParam::Typed { comptime, name, ty } => { + let mut out = String::new(); + if comptime.is_some() { + out.push_str("comptime "); + } + out.push_str(&ident_text(db, name)); + out.push_str(": "); + out.push_str(&format_type_ref(db, *ty)); + out + } + FuncParam::Untyped { comptime, name } => { + let mut out = String::new(); + if comptime.is_some() { + out.push_str("comptime "); + } + out.push_str(&ident_text(db, name)); + out + } + FuncParam::Error { .. } => "".to_owned(), + } +} + +fn format_pred_ref<'db>(db: &'db dyn HirDb, pred: hir::ast::ty::PredRef<'db>) -> String { + let pred = pred.kind(db); + let mut out = format!( + "{}: {}", + format_type_ref(db, pred.ty), + ident_text(db, &pred.class) + ); + if !pred.args.atom().is_empty() { + out.push('<'); + out.push_str( + &pred + .args + .atom() + .iter() + .map(|arg| format_type_ref(db, *arg)) + .collect::>() + .join(", "), + ); + out.push('>'); + } + out +} + +fn format_type_ref<'db>(db: &'db dyn HirDb, ty: TypeRef<'db>) -> String { + display_type_ref_source(db, ty) +} diff --git a/crates/hir-ty/src/infer/expr.rs b/crates/hir-ty/src/infer/expr.rs new file mode 100644 index 00000000..ec1c3a2d --- /dev/null +++ b/crates/hir-ty/src/infer/expr.rs @@ -0,0 +1,1364 @@ +use super::*; + +impl<'db> InferCtx<'db> { + pub(super) fn infer_expr( + &mut self, + body: FuncBody<'db>, + expr_id: Id>, + ) -> InferTy<'db> { + self.infer_expr_expected(body, expr_id, None) + } + + pub(super) fn infer_expr_expected( + &mut self, + body: FuncBody<'db>, + expr_id: Id>, + expected: Option>, + ) -> InferTy<'db> { + self.infer_expr_expected_impl(body, expr_id, expected, true) + } + + fn infer_expr_expected_without_final_check( + &mut self, + body: FuncBody<'db>, + expr_id: Id>, + expected: Option>, + ) -> InferTy<'db> { + self.infer_expr_expected_impl(body, expr_id, expected, false) + } + + fn infer_expr_expected_impl( + &mut self, + body: FuncBody<'db>, + expr_id: Id>, + expected: Option>, + check_expected: bool, + ) -> InferTy<'db> { + let expr = body.exprs(self.db).get(expr_id); + let mut ty = match &expr.kind { + ExprKind::Lit(lit) => self.infer_lit(body, expr_id, lit, expected.clone()), + ExprKind::Ident(name) => { + let resolution = self + .expr_resolutions + .get(&(body, expr_id)) + .cloned() + .unwrap_or(hir_nameres::Resolution::Err); + if matches!(resolution, hir_nameres::Resolution::DotCtorDeferred) { + self.infer_dot_ctor_expr( + body, + expr_id, + (*name.atom()).text(self.db), + &[], + expected.clone(), + ) + } else { + self.infer_resolution(body, expr_id, resolution) + } + } + ExprKind::DotCtor { name, args, .. } => self.infer_dot_ctor_expr( + body, + expr_id, + (*name.atom()).text(self.db), + args, + expected.clone(), + ), + ExprKind::Proxy { .. } => self.engine.fresh_var(), + ExprKind::Lambda { + params, + ret, + body: lambda_body, + } => self.infer_lambda( + self.expr_label_span(body, expr_id), + params.atom(), + *ret, + *lambda_body, + expected.clone(), + ), + ExprKind::BinOp { lhs, op, rhs } => { + self.infer_bin_op(body, expr_id, *lhs, *op.atom(), *rhs, expected.clone()) + } + ExprKind::Index { base, index } => { + if let Some(ret) = self.infer_storage_index_read(body, expr_id, *base, *index) { + ret + } else { + let base_ty = self.infer_expr(body, *base); + let index_ty = self.infer_expr(body, *index); + let ret = expected.clone().unwrap_or_else(|| self.engine.fresh_var()); + self.unify_expr( + body, + expr_id, + base_ty, + InferTy::Function { + params: vec![index_ty], + ret: Box::new(ret.clone()), + }, + ); + ret + } + } + ExprKind::Call { callee, args } => { + if let Some(ty) = + self.infer_constructor_call(body, expr_id, *callee, args, expected.clone()) + { + ty + } else { + self.infer_call_expr(body, expr_id, *callee, args, expected.clone()) + } + } + ExprKind::Field { base, .. } => { + if self.reject_storage_field_projection(body, expr_id, *base) { + InferTy::Error + } else { + let base_ty = (!self.is_namespace_expr(body, *base)) + .then(|| self.infer_expr(body, *base)); + let resolution = self.expr_resolutions.get(&(body, expr_id)).cloned(); + if let Some(resolution) = resolution { + self.infer_resolution(body, expr_id, resolution) + } else if let Some(base_ty) = base_ty + && let Some(field_ty) = + self.infer_adt_field_access(body, expr_id, *base, base_ty) + { + field_ty + } else { + self.emit_expr_error( + body, + expr_id, + TypeckDiagnostic::UnknownField { + span: self.field_label_span(body, expr_id), + field: self.field_name(body, expr_id), + }, + ); + InferTy::Error + } + } + } + ExprKind::Conversion { expr, ty } => { + let target = self.lower_type_ref(*ty); + let source = self.infer_expr(body, *expr); + self.pending_conversions.push(PendingConversion { + body, + expr: expr_id, + operand: *expr, + target_ref: *ty, + source, + target: target.clone(), + }); + target + } + ExprKind::TypeAscription { expr, ty } => { + let annot = self.lower_type_ref(*ty); + let expr_ty = self.infer_expr_expected(body, *expr, Some(annot.clone())); + self.unify_expr(body, *expr, annot.clone(), expr_ty); + annot + } + ExprKind::UnaryOp { op, expr } => { + self.infer_un_op(body, expr_id, *op.atom(), *expr, expected.clone()) + } + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + let input = self.if_expr_match_input(body, expr_id, *cond, *then_expr, *else_expr); + let cond_ty = self.infer_expr(body, input.cond); + let bool_ty = self.bool(); + self.unify_expr(body, input.cond, cond_ty, bool_ty); + let then_ty = self.infer_expr_expected(body, input.then_expr, expected.clone()); + let else_ty = self.infer_expr_expected(body, input.else_expr, expected.clone()); + if !self.report_numeric_if_branch_mismatch( + body, + expr_id, + input.then_expr, + then_ty.clone(), + input.else_expr, + else_ty.clone(), + ) { + self.unify_expr(body, input.else_expr, then_ty.clone(), else_ty); + } + then_ty + } + ExprKind::Tuple(elems) => self.infer_tuple_expr(body, expr_id, elems, expected.clone()), + ExprKind::Error => InferTy::Error, + }; + if check_expected + && let Some(expected) = expected + && !self.unify_expr(body, expr_id, expected, ty.clone()) + { + ty = InferTy::Error; + } + if self.expr_is_poisoned(body, expr_id) { + ty = InferTy::Error; + } + self.expr_tys.push((body, expr_id, ty.clone())); + ty + } + + fn infer_adt_field_access( + &mut self, + body: FuncBody<'db>, + expr: Id>, + base: Id>, + base_ty: InferTy<'db>, + ) -> Option> { + let normalized = self.normalize_aliases(base_ty.clone()); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } = self.engine.resolve(normalized) + else { + return None; + }; + let field_name = self.field_name(body, expr); + let (constructor, index) = self.lookup_adt_field_index(def, &field_name)?; + let ctor_ty = self.instantiate_adt_ctor(def, constructor, ObligationSource::Scheme); + let InferTy::Function { params, ret } = self.engine.resolve(ctor_ty) else { + return None; + }; + let field_ty = params.get(index as usize)?.clone(); + if !self.unify_expr(body, base, *ret, base_ty) { + return Some(InferTy::Error); + } + if !self + .adt_field_selections + .iter() + .any(|selection| selection.body == body && selection.expr == expr) + { + self.adt_field_selections.push(AdtFieldSelection { + body, + expr, + adt: def, + constructor, + index, + }); + } + Some(self.engine.resolve(field_ty)) + } + + fn report_numeric_if_branch_mismatch( + &mut self, + body: FuncBody<'db>, + if_expr: Id>, + then_expr: Id>, + then_ty: InferTy<'db>, + else_expr: Id>, + else_ty: InferTy<'db>, + ) -> bool { + if self.expr_has_integer_literal_obligation(body, then_expr) + && self.is_concrete_non_numeric(else_ty.clone()) + { + let actual = self.display_infer_ty(else_ty); + self.emit_error_with_poison( + TypeckDiagnostic::Mismatch { + span: self.expr_label_span(body, else_expr), + expected: "numeric".to_owned(), + actual, + }, + [ + PoisonTarget::Expr(body, then_expr), + PoisonTarget::Expr(body, if_expr), + ], + ); + return true; + } + if self.expr_has_integer_literal_obligation(body, else_expr) + && self.is_concrete_non_numeric(then_ty.clone()) + { + let actual = self.display_infer_ty(then_ty); + self.emit_error_with_poison( + TypeckDiagnostic::Mismatch { + span: self.expr_label_span(body, then_expr), + expected: "numeric".to_owned(), + actual, + }, + [ + PoisonTarget::Expr(body, else_expr), + PoisonTarget::Expr(body, if_expr), + ], + ); + return true; + } + false + } + + fn expr_has_integer_literal_obligation( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> bool { + self.pending.iter().any(|pending| { + pending.class == ClassId::Builtin(BuiltinClassId::Int) + && pending.args.is_empty() + && matches!( + pending.source, + ObligationSource::IntegerLiteral { + body: source_body, + expr: source_expr, + } if source_body == body && source_expr == expr + ) + }) + } + + fn infer_constructor_call( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + args: &[Id>], + expected: Option>, + ) -> Option> { + let resolution = self.expr_resolutions.get(&(body, callee_expr)).cloned()?; + match resolution { + hir_nameres::Resolution::Ctor { ty, index } => { + let source = self.call_site_source( + body, + call_expr, + callee_expr, + &hir_nameres::Resolution::Ctor { ty, index }, + ); + let ctor_ty = self.instantiate_adt_ctor( + ty, + index, + source.unwrap_or(ObligationSource::Scheme), + ); + let expected = expected.unwrap_or_else(|| self.engine.fresh_var()); + Some(self.apply_ctor_expr_scheme( + body, + call_expr, + ctor_ty, + args, + expected, + Some(CallSiteCallee::AdtCtor { ty, index }), + )) + } + hir_nameres::Resolution::Builtin(kind @ hir_nameres::BuiltinKind::Constructor(_)) => { + let source = self.call_site_source( + body, + call_expr, + callee_expr, + &hir_nameres::Resolution::Builtin(kind), + ); + let Some(scheme) = builtin_scheme(self.db, kind) else { + return Some(InferTy::Error); + }; + let instantiated = self.engine.instantiate_scheme_with_source( + scheme, + source.unwrap_or(ObligationSource::Scheme), + ); + let ctor_ty = self.accept_instantiated(instantiated); + let expected = expected.unwrap_or_else(|| self.engine.fresh_var()); + Some(self.apply_ctor_expr_scheme( + body, + call_expr, + ctor_ty, + args, + expected, + Some(CallSiteCallee::Builtin(kind)), + )) + } + hir_nameres::Resolution::DotCtorDeferred => { + let name = self.expr_constructor_name(body, callee_expr)?; + Some(self.infer_dot_ctor_expr(body, call_expr, &name, args, expected)) + } + _ => None, + } + } + + fn infer_call_expr( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + args: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let callee_ty = self.infer_callee_expr(body, call_expr, callee_expr); + let normalized = self.normalize_aliases(callee_ty.clone()); + let resolved = self.engine.resolve(normalized); + let site = DirectCallSite { + call_expr, + callee_expr, + callee: self + .expr_resolutions + .get(&(body, callee_expr)) + .and_then(|resolution| self.call_site_callee(resolution)), + }; + if matches!(resolved, InferTy::Error) { + for arg in args { + self.infer_expr(body, *arg); + } + self.poison_expr(body, call_expr); + return InferTy::Error; + } + if self.is_direct_call_callee(body, callee_expr) { + if let InferTy::Function { params, .. } = resolved { + self.infer_direct_call(body, site, callee_ty, Some(params), args, expected) + } else { + self.infer_direct_call(body, site, callee_ty, None, args, expected) + } + } else if matches!( + resolved, + InferTy::Error | InferTy::Unknown | InferTy::Var(_) + ) { + self.infer_direct_call(body, site, callee_ty, None, args, expected) + } else { + self.infer_indirect_call(body, call_expr, callee_expr, callee_ty, args, expected) + } + } + + fn infer_direct_call( + &mut self, + body: FuncBody<'db>, + site: DirectCallSite<'db>, + callee_ty: InferTy<'db>, + params: Option>>, + args: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + if let Some(params) = ¶ms + && params.len() != args.len() + { + self.emit_expr_error( + body, + site.call_expr, + TypeckDiagnostic::WrongArity { + span: self.expr_label_span(body, site.call_expr), + context: "call".to_owned(), + expected: params.len(), + actual: args.len(), + callee: site.callee.as_ref().and_then(|callee| { + callee_diagnostic_info(self.db, self.entry_module, callee) + }), + }, + ); + for (index, arg) in args.iter().enumerate() { + self.infer_expr_expected(body, *arg, params.get(index).cloned()); + } + return InferTy::Error; + } + let callee_name = self.comptime_callee_name(body, site.callee_expr); + let args = args + .iter() + .enumerate() + .map(|(index, arg)| { + if let Some(param) = params.as_ref().and_then(|params| params.get(index)) + && infer_ty_has_comptime_wrapper(&self.engine.resolve(param.clone())) + { + self.comptime_obligations.push(ComptimeObligation { + body, + expr: *arg, + kind: ComptimeObligationKind::CallParam { + call_expr: site.call_expr, + callee_expr: site.callee_expr, + function: callee_name.clone(), + param: format!("arg{index}"), + }, + }); + } + self.infer_call_arg_expected( + body, + *arg, + params + .as_ref() + .and_then(|params| params.get(index).cloned()), + site.callee.as_ref(), + index, + ) + }) + .collect::>(); + let ret = expected.unwrap_or_else(|| self.engine.fresh_var()); + self.unify_expr( + body, + site.call_expr, + callee_ty, + InferTy::Function { + params: args, + ret: Box::new(ret.clone()), + }, + ); + ret + } + + pub(super) fn infer_call_arg_expected( + &mut self, + body: FuncBody<'db>, + arg: Id>, + expected: Option>, + callee: Option<&CallSiteCallee<'db>>, + index: usize, + ) -> InferTy<'db> { + let Some(expected) = expected else { + return self.infer_expr(body, arg); + }; + let actual = + self.infer_expr_expected_without_final_check(body, arg, Some(expected.clone())); + let context = CallArgDiagnostic { + callee: callee + .and_then(|callee| callee_diagnostic_info(self.db, self.entry_module, callee)), + param: call_param_diagnostic_info(self.db, callee, index), + }; + if self.unify_call_arg(body, arg, expected, actual.clone(), context) { + actual + } else { + InferTy::Error + } + } + + fn infer_indirect_call( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + callee_ty: InferTy<'db>, + args: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let callable_sig = self.callable_sig_for_ty(callee_ty.clone()); + if let Some(sig) = &callable_sig + && sig.params.len() != args.len() + { + self.emit_expr_error( + body, + call_expr, + TypeckDiagnostic::WrongArity { + span: self.expr_label_span(body, call_expr), + context: "call".to_owned(), + expected: sig.params.len(), + actual: args.len(), + callee: None, + }, + ); + for (index, arg) in args.iter().enumerate() { + self.infer_expr_expected(body, *arg, sig.params.get(index).cloned()); + } + return InferTy::Error; + } + let inferred_args = args + .iter() + .enumerate() + .map(|(index, arg)| { + self.infer_expr_expected( + body, + *arg, + callable_sig + .as_ref() + .and_then(|sig| sig.params.get(index).cloned()), + ) + }) + .collect::>(); + let ret = expected.unwrap_or_else(|| self.engine.fresh_var()); + if let Some(sig) = callable_sig { + self.unify_expr(body, call_expr, sig.ret, ret.clone()); + } + let source = + self.indirect_call_site_source(body, call_expr, callee_expr, callee_ty.clone()); + self.pending.push(PendingObligation { + class: ClassId::Builtin(BuiltinClassId::Invokable), + main: callee_ty, + args: vec![invokable_arg_infer(inferred_args), ret.clone()], + source, + }); + ret + } + + fn expr_constructor_name(&self, body: FuncBody<'db>, expr: Id>) -> Option { + match &body.exprs(self.db).get(expr).kind { + ExprKind::Ident(name) => Some((*name.atom()).text(self.db).to_owned()), + ExprKind::Field { field, .. } => Some((*field.atom()).text(self.db).to_owned()), + _ => None, + } + } + + fn infer_callee_expr( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + ) -> InferTy<'db> { + match &body.exprs(self.db).get(callee_expr).kind { + ExprKind::Ident(_) => { + let resolution = self + .expr_resolutions + .get(&(body, callee_expr)) + .cloned() + .unwrap_or(hir_nameres::Resolution::Err); + let source = self.call_site_source(body, call_expr, callee_expr, &resolution); + self.infer_resolution_with_source( + body, + callee_expr, + resolution, + source, + ValuePosition::Callee, + ) + } + ExprKind::Field { base, .. } => { + if self.reject_storage_field_projection(body, callee_expr, *base) { + InferTy::Error + } else { + let base_ty = (!self.is_namespace_expr(body, *base)) + .then(|| self.infer_expr(body, *base)); + let resolution = self.expr_resolutions.get(&(body, callee_expr)).cloned(); + if let Some(resolution) = resolution { + let source = + self.call_site_source(body, call_expr, callee_expr, &resolution); + self.infer_resolution_with_source( + body, + callee_expr, + resolution, + source, + ValuePosition::Callee, + ) + } else if let Some(base_ty) = base_ty + && let Some(field_ty) = + self.infer_adt_field_access(body, callee_expr, *base, base_ty) + { + field_ty + } else { + self.emit_expr_error( + body, + callee_expr, + TypeckDiagnostic::UnknownField { + span: self.field_label_span(body, callee_expr), + field: self.field_name(body, callee_expr), + }, + ); + InferTy::Error + } + } + } + _ => self.infer_expr(body, callee_expr), + } + } + + fn call_site_source( + &self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option> { + let callee = self.call_site_callee(resolution)?; + Some(ObligationSource::CallSite { + body, + call_expr, + callee_expr, + callee, + }) + } + + fn call_site_callee( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option> { + Some(match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => CallSiteCallee::Function(*def), + hir_nameres::Resolution::Field(field) => CallSiteCallee::Field(*field), + hir_nameres::Resolution::Ctor { ty, index } => CallSiteCallee::AdtCtor { + ty: *ty, + index: *index, + }, + hir_nameres::Resolution::ClassMethod { class, name } => CallSiteCallee::ClassMethod { + class: *class, + name: name.clone(), + }, + hir_nameres::Resolution::Builtin( + kind @ (hir_nameres::BuiltinKind::Constructor(_) + | hir_nameres::BuiltinKind::Function(_) + | hir_nameres::BuiltinKind::ClassMethod(_)), + ) => CallSiteCallee::Builtin(*kind), + _ => return None, + }) + } + + fn indirect_call_site_source( + &mut self, + body: FuncBody<'db>, + call_expr: Id>, + callee_expr: Id>, + callee_ty: InferTy<'db>, + ) -> ObligationSource<'db> { + let callee = self + .closure_def_for_ty(callee_ty) + .map(CallSiteCallee::Closure) + .unwrap_or(CallSiteCallee::Invokable); + ObligationSource::CallSite { + body, + call_expr, + callee_expr, + callee, + } + } + + fn is_direct_call_callee(&self, body: FuncBody<'db>, callee_expr: Id>) -> bool { + self.expr_resolutions + .get(&(body, callee_expr)) + .is_some_and(is_direct_call_resolution) + } + + pub(super) fn callable_sig_for_ty(&mut self, ty: InferTy<'db>) -> Option> { + if let Some(sig) = self.closure_sig_for_ty(ty.clone()) { + return Some(sig); + } + let ty = self.normalize_aliases(ty); + match self.engine.resolve(ty) { + InferTy::Function { params, ret } => Some(ClosureSig { params, ret: *ret }), + _ => None, + } + } + + fn closure_def_for_ty(&mut self, ty: InferTy<'db>) -> Option> { + let ty = self.normalize_aliases(ty); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(ty) + else { + return None; + }; + if args.is_empty() && self.closure_sigs.contains_key(&def) { + Some(def) + } else { + None + } + } + + fn closure_sig_for_ty(&mut self, ty: InferTy<'db>) -> Option> { + let ty = self.normalize_aliases(ty); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(ty) + else { + return None; + }; + if !args.is_empty() { + return None; + } + self.closure_sigs.get(&def).cloned() + } + + fn infer_lit( + &mut self, + body: FuncBody<'db>, + expr: Id>, + lit: &LitKind, + expected: Option>, + ) -> InferTy<'db> { + match lit { + LitKind::Number(_) | LitKind::Hex(_) => { + let vid = self.engine.fresh_vid(); + let ty = InferTy::Var(vid); + self.pending.push(PendingObligation { + class: ClassId::Builtin(BuiltinClassId::Int), + main: ty.clone(), + args: Vec::new(), + source: ObligationSource::IntegerLiteral { body, expr }, + }); + ty + } + LitKind::String(_) => expected + .and_then(|expected| self.expected_string_lit_ty(expected)) + .unwrap_or_else(|| self.string()), + LitKind::Error => InferTy::Error, + } + } + + pub(super) fn expected_string_lit_ty( + &mut self, + expected: InferTy<'db>, + ) -> Option> { + let expected = self.normalize_aliases(expected); + if self.infer_ty_is_string_adt(expected.clone()) { + return Some(expected); + } + let InferTy::Comptime(inner) = self.engine.resolve(expected.clone()) else { + return None; + }; + self.infer_ty_is_string_adt(*inner).then_some(expected) + } + + fn infer_ty_is_string_adt(&mut self, ty: InferTy<'db>) -> bool { + let ty = self.normalize_aliases(ty); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(ty) + else { + return false; + }; + args.is_empty() && def.name(self.db).as_deref() == Some("string") + } + + fn infer_lambda( + &mut self, + span: LabelSpan, + params: &[FuncParam<'db>], + ret: Option>, + body: FuncBody<'db>, + expected: Option>, + ) -> InferTy<'db> { + let has_expected = expected.is_some(); + let (expected_params, expected_ret) = + self.expected_lambda_parts(span.clone(), expected, params.len()); + let ret_display = ret.map(|ret| self.display_type_ref(ret)); + let param_tys = params + .iter() + .enumerate() + .map(|(index, param)| { + let ty = match param { + FuncParam::Typed { comptime, ty, .. } => { + let ty = self.lower_type_ref(*ty); + let ty = self.maybe_comptime(*comptime, ty); + if let Some(expected) = expected_params + .as_ref() + .and_then(|params| params.get(index)) + { + self.unify_span(param.span(self.db), expected.clone(), ty.clone()); + } + ty + } + FuncParam::Untyped { comptime, .. } => { + let ty = expected_params + .as_ref() + .and_then(|params| params.get(index).cloned()) + .unwrap_or_else(|| self.engine.fresh_var()); + self.maybe_comptime(*comptime, ty) + } + FuncParam::Error { .. } => InferTy::Error, + }; + self.param_tys.insert((body, index as u32), ty.clone()); + ty + }) + .collect::>(); + let ret = if let Some(ret) = ret { + let annotated = self.lower_type_ref(ret); + if let Some(expected_ret) = expected_ret { + self.unify_span(ret.span(self.db), expected_ret, annotated.clone()); + } + annotated + } else { + expected_ret.unwrap_or_else(|| self.engine.fresh_var()) + }; + self.push_sail_scope(); + for (index, param) in params.iter().enumerate() { + if let Some(name) = param_name(self.db, param) { + let ty = self.param_ty(body, index as u32); + self.add_sail_local(name.to_owned(), ty); + } + } + self.return_stack.push(ret.clone()); + self.return_display_stack.push(ret_display); + self.infer_body(body); + self.return_display_stack.pop(); + self.return_stack.pop(); + self.pop_sail_scope(); + let fn_ty = InferTy::Function { + params: param_tys.clone(), + ret: Box::new(ret.clone()), + }; + if has_expected { + fn_ty + } else { + let closure_def = closure_def_id(self.db, body); + self.closure_sigs.insert( + closure_def, + ClosureSig { + params: param_tys, + ret, + }, + ); + InferTy::Named { + ctor: TyCtor::User(crate::UserTyCtor { + def: closure_def, + kind: crate::UserTyCtorKind::Adt, + }), + args: Vec::new(), + } + } + } + + fn expected_lambda_parts( + &mut self, + span: LabelSpan, + expected: Option>, + param_count: usize, + ) -> (Option>>, Option>) { + let Some(expected) = expected else { + return (None, None); + }; + let expected = self.normalize_aliases(expected); + match self.engine.resolve(expected.clone()) { + InferTy::Function { params, ret } => { + if params.len() != param_count { + self.diagnostics.push(TypeckDiagnostic::WrongArity { + span, + context: "lambda".to_owned(), + expected: params.len(), + actual: param_count, + callee: None, + }); + } + (Some(params), Some(*ret)) + } + InferTy::Var(_) | InferTy::Unknown => { + let params = (0..param_count) + .map(|_| self.engine.fresh_var()) + .collect::>(); + let ret = self.engine.fresh_var(); + self.unify_at( + span, + expected, + InferTy::Function { + params: params.clone(), + ret: Box::new(ret.clone()), + }, + ); + (Some(params), Some(ret)) + } + InferTy::Error => (None, None), + other => { + let actual = self.display_infer_ty(other); + self.diagnostics.push(TypeckDiagnostic::Mismatch { + span, + expected: "function".to_owned(), + actual, + }); + (None, None) + } + } + } + + fn infer_bin_op( + &mut self, + body: FuncBody<'db>, + expr: Id>, + lhs: Id>, + op: BinOp, + rhs: Id>, + expected: Option>, + ) -> InferTy<'db> { + let lhs_expr = lhs; + let rhs_expr = rhs; + match op { + BinOp::Add => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Add", "add", expected, + ), + BinOp::Sub => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Sub", "sub", expected, + ), + BinOp::Mul => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Mul", "mul", expected, + ), + BinOp::Div => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Div", "div", expected, + ), + BinOp::Mod => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Mod", "mod", expected, + ), + BinOp::Pow => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "Pow", "pow", expected, + ), + // EVM/Yul shift helpers take `(shift, value)`, while the source + // operators spell `value << shift` and `value >> shift`. + BinOp::Shl => self.infer_operator_function_call_expected( + body, expr, rhs_expr, lhs_expr, "bshlWord", expected, + ), + BinOp::Shr => self.infer_operator_function_call_expected( + body, expr, rhs_expr, lhs_expr, "bshrWord", expected, + ), + BinOp::BitAnd => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "BitAnd", "band", expected, + ), + BinOp::BitXor => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "BitXor", "bxor", expected, + ), + BinOp::BitOr => self.infer_operator_call_expected( + body, expr, lhs_expr, rhs_expr, "BitOr", "bor", expected, + ), + BinOp::Eq => { + let bool_ty = self.bool(); + self.infer_operator_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "Eq", + "eq", + Some(bool_ty), + ) + } + BinOp::NotEq => { + let bool_ty = self.bool(); + self.infer_operator_function_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "ne", + Some(bool_ty), + ) + } + BinOp::Lt => { + let bool_ty = self.bool(); + self.infer_operator_function_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "lt", + Some(bool_ty), + ) + } + BinOp::Gt => { + let bool_ty = self.bool(); + self.infer_operator_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "Ord", + "gt", + Some(bool_ty), + ) + } + BinOp::LtEq => { + let bool_ty = self.bool(); + self.infer_operator_function_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "le", + Some(bool_ty), + ) + } + BinOp::GtEq => { + let bool_ty = self.bool(); + self.infer_operator_function_call_expected( + body, + expr, + lhs_expr, + rhs_expr, + "ge", + Some(bool_ty), + ) + } + BinOp::And | BinOp::Or => { + // Logical operators are language-level control flow, not + // overloadable calls. Keeping both operands as explicit bool + // expressions lets specialization preserve the RHS until Hull + // lowers the operator to a conditional. + let bool_ty = self.bool(); + self.infer_expr_expected(body, lhs_expr, Some(bool_ty.clone())); + self.infer_expr_expected(body, rhs_expr, Some(bool_ty.clone())); + bool_ty + } + BinOp::Error => InferTy::Error, + } + } + + #[allow(clippy::too_many_arguments)] + fn infer_operator_call_expected( + &mut self, + body: FuncBody<'db>, + expr: Id>, + lhs: Id>, + rhs: Id>, + class_name: &str, + method: &str, + expected: Option>, + ) -> InferTy<'db> { + let Some((class, name)) = self.lookup_operator_class_method(class_name, method) else { + self.infer_expr(body, lhs); + self.infer_expr(body, rhs); + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsatisfiedConstraint { + span: self.expr_label_span(body, expr), + pred: format!("operator {class_name}.{method}"), + }, + ); + return InferTy::Error; + }; + + let source = ObligationSource::CallSite { + body, + call_expr: expr, + callee_expr: expr, + callee: CallSiteCallee::ClassMethod { + class, + name: name.clone(), + }, + }; + let callee_ty = self.instantiate_class_method(class, &name, source); + if let Some(expected_ty) = expected.clone() { + let normalized = self.normalize_aliases(callee_ty.clone()); + if let InferTy::Function { params, .. } = self.engine.resolve(normalized) { + self.unify_expr( + body, + expr, + callee_ty.clone(), + InferTy::Function { + params, + ret: Box::new(expected_ty), + }, + ); + } + } + let normalized = self.normalize_aliases(callee_ty.clone()); + let resolved = self.engine.resolve(normalized); + let params = match resolved { + InferTy::Function { params, .. } => Some(params), + _ => None, + }; + self.infer_direct_call( + body, + DirectCallSite { + call_expr: expr, + callee_expr: expr, + callee: Some(CallSiteCallee::ClassMethod { class, name }), + }, + callee_ty, + params, + &[lhs, rhs], + expected, + ) + } + + #[allow(clippy::too_many_arguments)] + fn infer_operator_function_call_expected( + &mut self, + body: FuncBody<'db>, + expr: Id>, + lhs: Id>, + rhs: Id>, + name: &str, + expected: Option>, + ) -> InferTy<'db> { + let Some(resolution) = self.lookup_operator_function(name) else { + self.infer_expr(body, lhs); + self.infer_expr(body, rhs); + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsatisfiedConstraint { + span: self.expr_label_span(body, expr), + pred: format!("operator {name}"), + }, + ); + return InferTy::Error; + }; + + let callee = self.call_site_callee(&resolution); + let source = self.call_site_source(body, expr, expr, &resolution); + let callee_ty = self.infer_resolution_with_source( + body, + expr, + resolution, + source, + ValuePosition::Callee, + ); + let normalized = self.normalize_aliases(callee_ty.clone()); + let resolved = self.engine.resolve(normalized); + let params = match resolved { + InferTy::Function { params, .. } => Some(params), + _ => None, + }; + self.infer_direct_call( + body, + DirectCallSite { + call_expr: expr, + callee_expr: expr, + callee, + }, + callee_ty, + params, + &[lhs, rhs], + expected, + ) + } + + fn infer_operator_unary_function_call_expected( + &mut self, + body: FuncBody<'db>, + expr: Id>, + arg: Id>, + name: &str, + expected: Option>, + ) -> InferTy<'db> { + let Some(resolution) = self.lookup_operator_function(name) else { + self.infer_expr(body, arg); + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsatisfiedConstraint { + span: self.expr_label_span(body, expr), + pred: format!("operator {name}"), + }, + ); + return InferTy::Error; + }; + + let callee = self.call_site_callee(&resolution); + let source = self.call_site_source(body, expr, expr, &resolution); + let callee_ty = self.infer_resolution_with_source( + body, + expr, + resolution, + source, + ValuePosition::Callee, + ); + let normalized = self.normalize_aliases(callee_ty.clone()); + let resolved = self.engine.resolve(normalized); + let params = match resolved { + InferTy::Function { params, .. } => Some(params), + _ => None, + }; + self.infer_direct_call( + body, + DirectCallSite { + call_expr: expr, + callee_expr: expr, + callee, + }, + callee_ty, + params, + &[arg], + expected, + ) + } + + fn lookup_operator_class_method( + &self, + class_name: &str, + method: &str, + ) -> Option<(DefId<'db>, String)> { + let qualified = format!("{class_name}.{method}"); + if let Some(module_id) = module_id_for_hir_module(self.db, self.module) { + let env = nameres::module_import_surface(self.db, module_id); + let local = env + .item_scope + .as_ref() + .and_then(|scope| scope.term_resolution(&qualified)); + if let Some(resolution) = local.or_else(|| env.terms.get(&qualified).cloned()) + && let Some(method) = class_method_resolution(resolution, method) + { + return Some(method); + } + if let Some(method) = + self.lookup_imported_operator_class_method(module_id, &qualified, method) + { + return Some(method); + } + return unique_visible_class_method(&env.terms, &qualified, method); + } + + hir_nameres::item_scope_facts(self.db, self.module) + .term_resolution(&qualified) + .and_then(|resolution| class_method_resolution(resolution, method)) + } + + fn lookup_imported_operator_class_method( + &self, + module_id: ModuleId<'db>, + qualified: &str, + method: &str, + ) -> Option<(DefId<'db>, String)> { + let file = self.db.module_file(module_id)?; + let imports = nameres::module_imports(self.db, file); + let mut found = None; + for path in imports.import_refs { + let Ok(imported_module) = nameres::resolve_module_path(self.db, module_id, path) else { + continue; + }; + let env = nameres::module_import_surface(self.db, imported_module); + let local = env + .item_scope + .as_ref() + .and_then(|scope| scope.term_resolution(qualified)); + let candidate = local + .or_else(|| env.terms.get(qualified).cloned()) + .and_then(|resolution| class_method_resolution(resolution, method)) + .or_else(|| unique_visible_class_method(&env.terms, qualified, method)); + let Some(candidate) = candidate else { + continue; + }; + if found + .as_ref() + .is_some_and(|existing| existing != &candidate) + { + return None; + } + found = Some(candidate); + } + found + } + + fn lookup_operator_function(&self, name: &str) -> Option> { + if let Some(module_id) = module_id_for_hir_module(self.db, self.module) { + let env = nameres::module_import_surface(self.db, module_id); + let local = env + .item_scope + .as_ref() + .and_then(|scope| scope.term_resolution(name)); + return local.or_else(|| env.terms.get(name).cloned()); + } + + hir_nameres::item_scope_facts(self.db, self.module).term_resolution(name) + } + + pub(super) fn is_storage_index_word_numeric(&mut self, ty: InferTy<'db>) -> bool { + let ty = self.normalize_aliases(ty); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(ty) + else { + return false; + }; + args.is_empty() && matches!(def.name(self.db).as_deref(), Some("uint") | Some("uint256")) + } + + fn infer_un_op( + &mut self, + body: FuncBody<'db>, + operator_expr: Id>, + op: UnOp, + operand: Id>, + expected: Option>, + ) -> InferTy<'db> { + match op { + UnOp::Not => self.infer_operator_unary_function_call_expected( + body, + operator_expr, + operand, + "not", + expected, + ), + UnOp::Error => InferTy::Error, + } + } +} diff --git a/crates/hir-ty/src/infer/lookup.rs b/crates/hir-ty/src/infer/lookup.rs new file mode 100644 index 00000000..bf5be86a --- /dev/null +++ b/crates/hir-ty/src/infer/lookup.rs @@ -0,0 +1,383 @@ +pub(super) use hir_nameres::{ + ident_text, is_direct_call_resolution, param_bindings, type_var_bindings, +}; + +use super::*; + +pub(super) struct FunctionLookup<'db> { + pub(super) function: FunctionDef<'db>, + pub(super) type_vars: Vec>, + pub(super) enclosing_contract: Option>, +} + +pub(super) struct FieldLookup<'db> { + pub(super) field: FieldDef<'db>, + pub(super) type_vars: Vec>, +} + +pub(super) struct AdtLookup<'db> { + pub(super) adt: AdtDef<'db>, + pub(super) type_vars: Vec>, +} + +pub(super) struct TypeAliasLookup<'db> { + pub(super) alias: TypeAlias<'db>, + pub(super) type_vars: Vec>, + pub(super) inherited_type_var_count: usize, +} + +pub(super) struct ClassLookup<'db> { + pub(super) class: ClassDef<'db>, +} + +pub(super) fn find_function_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_function_in_item(db, *item, def, &[], None)) +} + +fn find_function_in_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + enclosing_contract: Option>, +) -> Option> { + match item { + Item::FunctionDef(function) if function.def_id_value(db) == def => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(sig_type_vars(function.def_id_value(db), function.sig(db))); + Some(FunctionLookup { + function, + type_vars, + enclosing_contract, + }) + } + Item::InstanceDef(instance) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + )); + instance.methods(db).iter().find_map(|method| { + find_function_in_item(db, Item::FunctionDef(*method), def, &inherited, None) + }) + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + contract.items(db).iter().find_map(|item| match *item { + ContractItem::FunctionDef(function) => find_function_in_item( + db, + Item::FunctionDef(function), + def, + &inherited, + Some(contract.def_id_value(db)), + ), + ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }) + } + _ => None, + } +} + +pub(super) fn find_field_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + field: hir_nameres::FieldId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| { + let Item::ContractDef(contract) = item else { + return None; + }; + if contract.def_id_value(db) != field.contract { + return None; + } + let type_vars = type_var_bindings(contract.def_id_value(db), contract.ty_param_elems(db)); + let field = contract.fields(db).get(field.index.as_usize())?.clone(); + Some(FieldLookup { field, type_vars }) + }) +} + +pub(super) fn find_adt_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_adt_in_item(db, *item, def, &[])) +} + +fn find_adt_in_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + match item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + adt.def_id_value(db), + adt.ty_param_elems(db), + )); + Some(AdtLookup { adt, type_vars }) + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + contract.items(db).iter().find_map(|item| match *item { + ContractItem::AdtDef(adt) => { + find_adt_in_item(db, Item::AdtDef(adt), def, &inherited) + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::Error { .. } => None, + }) + } + _ => None, + } +} + +pub(super) fn find_type_alias_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_type_alias_in_item(db, *item, def, inherited)) +} + +fn find_type_alias_in_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + def: DefId<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], +) -> Option> { + match item { + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + let inherited_type_var_count = inherited.len(); + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + alias.def_id_value(db), + alias.ty_param_elems(db), + )); + Some(TypeAliasLookup { + alias, + type_vars, + inherited_type_var_count, + }) + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + contract.items(db).iter().find_map(|item| match *item { + ContractItem::TypeAlias(alias) => { + find_type_alias_in_item(db, Item::TypeAlias(alias), def, &inherited) + } + ContractItem::FunctionDef(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }) + } + _ => None, + } +} + +pub(super) fn find_class_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| { + let Item::ClassDef(class) = item else { + return None; + }; + if class.def_id_value(db) != def { + return None; + } + Some(ClassLookup { class: *class }) + }) +} + +pub(super) fn sig_type_vars<'db>( + owner: DefId<'db>, + sig: &hir::ast::function::FuncSig<'db>, +) -> Vec> { + type_var_bindings(owner, &sig.type_vars) +} + +pub(super) fn substitute_infer_alias_args<'db>( + ty: InferTy<'db>, + args: &[InferTy<'db>], +) -> InferTy<'db> { + match ty { + InferTy::BoundVar(index) => args + .get(index as usize) + .cloned() + .unwrap_or(InferTy::BoundVar(index)), + InferTy::Named { ctor, args: inner } => InferTy::Named { + ctor, + args: inner + .into_iter() + .map(|arg| substitute_infer_alias_args(arg, args)) + .collect(), + }, + InferTy::Function { params, ret } => InferTy::Function { + params: params + .into_iter() + .map(|param| substitute_infer_alias_args(param, args)) + .collect(), + ret: Box::new(substitute_infer_alias_args(*ret, args)), + }, + InferTy::Tuple(elems) => InferTy::Tuple( + elems + .into_iter() + .map(|elem| substitute_infer_alias_args(elem, args)) + .collect(), + ), + InferTy::Comptime(inner) => { + InferTy::Comptime(Box::new(substitute_infer_alias_args(*inner, args))) + } + ty @ (InferTy::Error | InferTy::Unknown | InferTy::Var(_)) => ty, + } +} + +pub(super) fn param_names<'db>(db: &'db dyn HirDb, params: &[FuncParam<'db>]) -> Vec { + params + .iter() + .filter_map(|param| param_name(db, param).map(str::to_owned)) + .collect() +} + +pub(super) fn partial_data_entries( + env: &nameres::ModuleImportSurface<'_>, +) -> Vec<(String, Vec)> { + env.partial_data + .iter() + .map(|(name, ctors)| (name.clone(), ctors.iter().cloned().collect())) + .collect() +} + +pub(super) fn closure_def_id<'db>(db: &'db dyn Db, body: FuncBody<'db>) -> DefId<'db> { + let body_def = body.def_id(db); + DefId::new( + db, + body_def.file(db), + Some(body_def), + DefKind::Adt, + Some("t_closure".to_owned()), + body_def.fingerprint(db), + Disambiguator::ZERO, + ) +} + +pub(super) fn invokable_arg_infer<'db>(args: Vec>) -> InferTy<'db> { + product_infer_ty(args) +} + +pub(super) fn file_url_tail(db: &dyn HirDb, file: hir::input::SourceFile) -> String { + let url = file.url(db); + if let Some(mut segments) = url.path_segments() + && let Some(last) = segments.next_back() + && !last.is_empty() + { + return last.to_owned(); + } + url.as_str() + .rsplit('/') + .next() + .filter(|tail| !tail.is_empty()) + .unwrap_or(url.as_str()) + .to_owned() +} + +pub(super) fn param_name<'db>(db: &'db dyn HirDb, param: &FuncParam<'db>) -> Option<&'db str> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some((*name.atom()).text(db)) + } + FuncParam::Error { .. } => None, + } +} + +pub(super) fn body_expr_resolution<'a, 'db>( + body_map: &'a hir_nameres::BodyResolutionMap<'db>, + body: FuncBody<'db>, + expr: Id>, +) -> Option<&'a hir_nameres::Resolution<'db>> { + body_map + .exprs + .iter() + .find(|entry| entry.body == body && entry.expr == expr) + .map(|entry| &entry.resolution) +} + +pub(super) fn ty_is_closed_concrete<'db>(db: &'db dyn HirDb, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + TyKind::Named { args, .. } | TyKind::Tuple(args) => { + args.iter().all(|arg| ty_is_closed_concrete(db, *arg)) + } + TyKind::Function { params, ret } => { + params.iter().all(|param| ty_is_closed_concrete(db, *param)) + && ty_is_closed_concrete(db, *ret) + } + TyKind::Comptime(inner) => ty_is_closed_concrete(db, *inner), + } +} + +pub(super) fn expr_is_literal_comptime<'db>( + db: &'db dyn HirDb, + body: FuncBody<'db>, + expr: Id>, +) -> bool { + match &body.exprs(db).get(expr).kind { + ExprKind::Lit(_) | ExprKind::Proxy { .. } => true, + ExprKind::Tuple(elems) | ExprKind::DotCtor { args: elems, .. } => elems + .iter() + .all(|elem| expr_is_literal_comptime(db, body, *elem)), + ExprKind::Conversion { expr, .. } + | ExprKind::TypeAscription { expr, .. } + | ExprKind::UnaryOp { expr, .. } => expr_is_literal_comptime(db, body, *expr), + ExprKind::BinOp { lhs, rhs, .. } => { + expr_is_literal_comptime(db, body, *lhs) && expr_is_literal_comptime(db, body, *rhs) + } + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + expr_is_literal_comptime(db, body, *cond) + && expr_is_literal_comptime(db, body, *then_expr) + && expr_is_literal_comptime(db, body, *else_expr) + } + ExprKind::Ident(_) + | ExprKind::Call { .. } + | ExprKind::Field { .. } + | ExprKind::Index { .. } + | ExprKind::Lambda { .. } + | ExprKind::Error => false, + } +} diff --git a/crates/hir-ty/src/infer/mod.rs b/crates/hir-ty/src/infer/mod.rs new file mode 100644 index 00000000..0f40f857 --- /dev/null +++ b/crates/hir-ty/src/infer/mod.rs @@ -0,0 +1,568 @@ +//! Ephemeral type inference over HIR bodies. + +use std::marker::PhantomData; + +use ena::unify::{InPlaceUnificationTable, NoError, UnifyKey, UnifyValue}; +use hir::{ + Db as HirDb, + anchor::{DefId, DefKind, Disambiguator}, + arena::{Arena, Id}, + ast::{ + function::{ + AssignOp, BinOp, Expr, ExprKind, FuncBody, FuncParam, FuncSig, LitKind, MatchArm, Pat, + PatKind, Stmt, StmtKind, UnOp, YulCase, YulExpr, YulExprKind, YulLitKind, YulStmt, + YulStmtKind, + }, + item::{ + AdtCtor, AdtDef, ClassDef, ContractDef, ContractItem, ContractKind, FieldDef, FuncKind, + FunctionDef, Item, Module, TypeAlias, TypeAliasKind, + }, + ty::{TypeRef, TypeRefKind}, + }, + diag::{AnyDiagnostic, Diagnostic, DiagnosticCode, LabelSpan, sort_dedup_query_diagnostics}, + nameres as hir_nameres, + span::{Span, Spanned}, +}; +use matchcov::{ + Arm as CoverageArm, ConstructorFields, ConstructorOracle, ConstructorSpace, Exhaustiveness, + Reachability, UnreachableReason, Witness as CoverageWitness, +}; +use nameres::{LibraryId, ModuleId, module_id_from_key, module_key_for_path}; +use parser::{parse_diagnostics, parse_file_to_hir}; +use rustc_hash::{FxHashMap, FxHashSet}; +use tracing::field; + +use crate::{ + BinderEnv, BodyDesugarView, BodyPreTypeckDesugarPlan, BoolUnitSumView, BuiltinClassId, + BuiltinTyCtor, ClassId, Db, LoweredFunction, Pred, PredKind, ProductShape, QualTy, + SourceOrigin, Ty, TyCtor, TyKind, TyScheme, TypeLowering, TypeLoweringDiagnostic, + UserTyCtorKind, + alias::{AliasError, AliasNormalizer, AliasType, AliasTypeKind}, + builtin_scheme, canonical_goal_with_allowed, class_method_type_vars, + contract::{module_contract_diagnostics, module_manual_generic_abi_diagnostics}, + coverage::{BuiltinCoverageCtor, CoverageCtor, CoverageHead, CoveragePat}, + solver::{ + DerivedClauseKind, Evidence, Solution, Substitution, TraitEnvId, collect_evidence_vars, + collect_pred_vars, collect_ty_vars, instance_soundness_diagnostics, solve_report, + }, + trait_env_with_givens, type_alias_normalization_errors, + value_type_underlying_has_word_storage_representation, value_type_underlying_in_context, +}; + +mod comptime; +mod coverage_adapter; +mod ctx; +mod desugar_view; +mod diagnostics; +mod expr; +mod lookup; +mod obligations; +mod origin; +mod pattern; +mod schemes; +mod stmt; +mod storage; +mod table; +mod unify; +mod yul; + +#[cfg(test)] +mod tests; + +use self::{ + comptime::*, ctx::*, desugar_view::*, diagnostics::*, lookup::*, obligations::*, origin::*, + schemes::*, +}; +pub use self::{ + ctx::{body_ty_diagnostics, infer_body}, + diagnostics::{ + CalleeDiagnostic, ParameterDiagnostic, TypeckDiagnostic, ValueNamespace, ValuePosition, + }, + schemes::{ + adt_ctor_scheme, class_method_scheme, field_scheme, function_scheme, + lower_normalized_function_with_inferred_signature, module_typeck_diagnostics, + reachable_typeck_diagnostics, + }, + table::{InferTable, InferTy, Instantiated, TyVid, UnifyError, VarValue}, +}; + +/// Type-checking context for one body inference query. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyTyContext<'db> { + /// HIR module containing the root body. + pub module: Module<'db>, + /// Driver module id used to resolve imported definition schemes. + pub entry_module: Option>, + /// Nameres result for the body and any lambdas nested inside it. + pub name_resolution: hir_nameres::BodyResolutionMap<'db>, + /// Type variables visible in this body. + pub type_vars: Vec>, + /// Parameter names in source order for Yul/assembly SAIL references. + pub param_names: Vec, + /// Parameter types in source order for the root body. + pub params: Vec>, + /// Expected return type for the root body, when known from a signature. + pub ret: Option>, + /// Source spelling for the expected return type, when it comes from user syntax. + pub ret_display: Option, + /// Trait environment used to solve deferred class obligations. + pub trait_env: Option>, + /// Imported data types whose constructors are only partially visible. + pub partial_data: Vec<(String, Vec)>, + /// Pre-typecheck desugar facts for the root body and nested lambda bodies. + pub pre_typeck_desugar: Vec>, +} + +/// Scheme for a resolved ADT constructor. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct AdtCtorScheme<'db> { + /// Owning ADT definition. + pub ty: DefId<'db>, + /// Constructor index in the owning ADT. + pub index: hir_nameres::CtorIndex, + /// Constructor leaf name. + pub name: String, + /// Polymorphic constructor scheme. + pub scheme: TyScheme<'db>, +} + +/// Ground type assigned to an expression. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ExprTy<'db> { + /// Body containing the expression. + pub body: FuncBody<'db>, + /// Expression ID. + pub expr: Id>, + /// Ground type or `Ty::unknown`. + pub ty: Ty<'db>, +} + +/// Ground type assigned to a pattern. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct PatTy<'db> { + /// Body containing the pattern. + pub body: FuncBody<'db>, + /// Pattern ID. + pub pat: Id>, + /// Ground type or `Ty::unknown`. + pub ty: Ty<'db>, +} + +/// Ground type assigned to a let binding. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct LetTy<'db> { + /// Body containing the let statement. + pub body: FuncBody<'db>, + /// Let statement ID. + pub stmt: Id>, + /// Ground type or `Ty::unknown`. + pub ty: Ty<'db>, +} + +/// Type-directed selection of a named struct field. +/// +/// Name resolution cannot choose this field until the base expression type is +/// known, so inference records the semantic owner and positional index for +/// downstream lowering. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct AdtFieldSelection<'db> { + /// Body containing the field access. + pub body: FuncBody<'db>, + /// Field-access expression ID. + pub expr: Id>, + /// Struct ADT that owns the field. + pub adt: DefId<'db>, + /// Constructor containing the struct fields. + pub constructor: hir_nameres::CtorIndex, + /// Zero-based field index in source order. + pub index: u32, +} + +/// A source-level conversion validated after inference has resolved its types. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct CheckedConversion<'db> { + /// Body containing the conversion. + pub body: FuncBody<'db>, + /// Conversion expression ID. + pub expr: Id>, + /// Alias-normalized source type. + pub source: Ty<'db>, + /// Alias-normalized target type. + pub target: Ty<'db>, + /// Runtime conversion semantics. + pub kind: ConversionKind, +} + +/// Runtime semantics of a validated conversion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum ConversionKind { + /// Source and target are the same alias-normalized resolved type. + Identity, + /// Wrap an underlying value in its nominal user-defined value type. + ValueTypeWrap, + /// Unwrap a nominal user-defined value type to its underlying value. + ValueTypeUnwrap, +} + +/// Source of a deferred obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ObligationSource<'db> { + /// Obligation created by an integer literal. + IntegerLiteral { + /// Body containing the literal. + body: FuncBody<'db>, + /// Literal expression. + expr: Id>, + }, + /// Obligation instantiated from a scheme. + Scheme, + /// Obligation instantiated while typing a call callee. + CallSite { + /// Body containing the call. + body: FuncBody<'db>, + /// Call expression. + call_expr: Id>, + /// Expression used as the callee. + callee_expr: Id>, + /// Resolved callee identity. + callee: CallSiteCallee<'db>, + }, + /// Obligation instantiated from a class-method expression. + ClassMethod { + /// Body containing the class-method expression. + body: FuncBody<'db>, + /// Expression that resolved to the class method. + expr: Id>, + }, + /// Obligation created by an integer literal pattern. + IntegerLiteralPattern { + /// Body containing the literal pattern. + body: FuncBody<'db>, + /// Literal pattern. + pat: Id>, + }, +} + +/// Resolved callable identity attached to a call-site obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum CallSiteCallee<'db> { + /// User function or method. + Function(DefId<'db>), + /// Lambda closure value synthesized by inference. + Closure(DefId<'db>), + /// Callable value invoked through the builtin `invokable` class. + Invokable, + /// Contract field used as a callable value. + Field(hir_nameres::FieldId<'db>), + /// Algebraic data constructor. + AdtCtor { + /// Owning ADT. + ty: DefId<'db>, + /// Constructor index. + index: hir_nameres::CtorIndex, + }, + /// Class method. + ClassMethod { + /// Owning class. + class: DefId<'db>, + /// Method name. + name: String, + }, + /// Builtin callable. + Builtin(hir_nameres::BuiltinKind), +} + +/// Deferred class obligation published by inference. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DeferredObligation<'db> { + /// Predicate that remains for the future solver. + pub pred: Pred<'db>, + /// Origin of this obligation. + pub source: ObligationSource<'db>, +} + +/// Evidence recorded for a solved deferred obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ObligationEvidence<'db> { + /// Index into [`InferenceResult::obligations`]. + pub obligation: usize, + /// Solver evidence for the obligation. + pub evidence: Evidence<'db>, +} + +/// Evidence addressable by the expression that triggered a constrained call. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct CallSiteEvidence<'db> { + /// Body containing the call. + pub body: FuncBody<'db>, + /// Call expression. + pub call_expr: Id>, + /// Expression used as the callee. + pub callee_expr: Id>, + /// Resolved callee identity. + pub callee: CallSiteCallee<'db>, + /// Index into [`InferenceResult::obligations`]. + pub obligation: usize, + /// Solver evidence for the call-site obligation. + pub evidence: Evidence<'db>, +} + +/// Deferred comptime check that must be validated after specialization. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ComptimeObligation<'db> { + /// Body containing the expression that must be comptime. + pub body: FuncBody<'db>, + /// Expression that must reduce to a comptime value. + pub expr: Id>, + /// Obligation origin. + pub kind: ComptimeObligationKind<'db>, +} + +/// Source of a deferred comptime obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ComptimeObligationKind<'db> { + /// Initializer of a comptime or inferred-`integer` let binding. + LetInit { + /// Let statement. + stmt: Id>, + /// Binding name. + name: String, + }, + /// Return expression of a `returns (comptime T)` body. + Return { + /// Function or lambda context. + context: String, + }, + /// Argument passed to a comptime parameter. + CallParam { + /// Call expression. + call_expr: Id>, + /// Callee expression. + callee_expr: Id>, + /// Callable display name. + function: String, + /// Parameter display name. + param: String, + }, + /// Expression label in a `comptime` match pattern. + PatternLabel { + /// Pattern containing the label. + pat: Id>, + }, +} + +/// Body inference result. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct InferenceResult<'db> { + /// Generalized function type inferred for the root body. + pub root_scheme: TyScheme<'db>, + /// Expression type table. + pub expr_tys: Vec>, + /// Pattern type table. + pub pat_tys: Vec>, + /// Let binding type table. + pub let_tys: Vec>, + /// Named struct fields selected after the base expression type was known. + pub adt_field_selections: Vec>, + /// Source conversions whose semantics were validated after inference. + pub checked_conversions: Vec>, + /// Deferred obligations that the future solver must resolve. + pub obligations: Vec>, + /// Evidence for obligations solved by the trait solver. + pub obligation_evidence: Vec>, + /// Evidence indexed by constrained call expression. + pub call_site_evidence: Vec>, + /// Deferred comptime checks for the backend/specializer. + pub comptime_obligations: Vec>, + /// Type-checking diagnostics found while inferring this body. + pub diagnostics: Vec, +} + +/// Convenience lookups on an inference result. +pub trait InferResultExt<'db> { + /// Returns the recorded type for `expr` in `body`. + fn expr_ty(&self, body: FuncBody<'db>, expr: Id>) -> Option>; + + /// Returns the recorded type for `pat` in `body`. + fn pat_ty(&self, body: FuncBody<'db>, pat: Id>) -> Option>; + + /// Returns the recorded type for a let statement in `body`. + fn let_ty(&self, body: FuncBody<'db>, stmt: Id>) -> Option>; +} + +impl<'db> InferResultExt<'db> for InferenceResult<'db> { + fn expr_ty(&self, body: FuncBody<'db>, expr: Id>) -> Option> { + self.expr_tys + .iter() + .find(|entry| entry.body == body && entry.expr == expr) + .map(|entry| entry.ty) + } + + fn pat_ty(&self, body: FuncBody<'db>, pat: Id>) -> Option> { + self.pat_tys + .iter() + .find(|entry| entry.body == body && entry.pat == pat) + .map(|entry| entry.ty) + } + + fn let_ty(&self, body: FuncBody<'db>, stmt: Id>) -> Option> { + self.let_tys + .iter() + .find(|entry| entry.body == body && entry.stmt == stmt) + .map(|entry| entry.ty) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PendingObligation<'db> { + class: ClassId<'db>, + main: InferTy<'db>, + args: Vec>, + source: ObligationSource<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PendingEqualityError<'db> { + source: ObligationSource<'db>, + error: UnifyError<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PendingConversion<'db> { + body: FuncBody<'db>, + expr: Id>, + operand: Id>, + target_ref: TypeRef<'db>, + source: InferTy<'db>, + target: InferTy<'db>, +} + +fn ty_is_resolved_for_conversion<'db>(db: &'db dyn HirDb, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Error | TyKind::Unknown => false, + TyKind::BoundVar(_) => true, + TyKind::Named { args, .. } => args + .iter() + .all(|arg| ty_is_resolved_for_conversion(db, *arg)), + TyKind::Function { params, ret } => { + params + .iter() + .all(|param| ty_is_resolved_for_conversion(db, *param)) + && ty_is_resolved_for_conversion(db, *ret) + } + TyKind::Tuple(elems) => elems + .iter() + .all(|elem| ty_is_resolved_for_conversion(db, *elem)), + TyKind::Comptime(inner) => ty_is_resolved_for_conversion(db, *inner), + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum InstantiatedPred<'db> { + Obligation(PendingObligation<'db>), + EqualityError(PendingEqualityError<'db>), + None, +} + +#[derive(Debug, Clone)] +struct PendingComptimeLet<'db> { + body: FuncBody<'db>, + stmt: Id>, + expr: Id>, + name: String, + declared: bool, + ty: InferTy<'db>, +} + +#[derive(Debug, Clone)] +struct DirectCallSite<'db> { + call_expr: Id>, + callee_expr: Id>, + callee: Option>, +} + +#[derive(Debug, Clone)] +struct CallArgDiagnostic { + callee: Option, + param: ParameterDiagnostic, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ClosureSig<'db> { + params: Vec>, + ret: InferTy<'db>, +} + +enum DotCtorLookup<'db> { + Match { + ty: InferTy<'db>, + callee: CallSiteCallee<'db>, + }, + NoExpected, + NoMatch, + Ambiguous(Vec), +} + +impl<'db> BodyTyContext<'db> { + /// Creates a body type-checking context. + pub fn new( + module: Module<'db>, + name_resolution: hir_nameres::BodyResolutionMap<'db>, + type_vars: Vec>, + params: Vec>, + ret: Option>, + ) -> Self { + Self { + module, + entry_module: None, + name_resolution, + type_vars, + param_names: Vec::new(), + params, + ret, + ret_display: None, + trait_env: None, + partial_data: Vec::new(), + pre_typeck_desugar: Vec::new(), + } + } + + /// Adds root parameter names to the context. + pub fn with_param_names(mut self, param_names: Vec) -> Self { + self.param_names = param_names; + self + } + + /// Adds source spelling for the expected root return type. + pub fn with_ret_display(mut self, ret_display: Option) -> Self { + self.ret_display = ret_display; + self + } + + /// Adds the driver module id used for imported scheme lookup. + pub fn with_entry_module(mut self, module: ModuleId<'db>) -> Self { + self.entry_module = Some(module); + self + } + + /// Adds the trait environment used to solve deferred obligations. + pub fn with_trait_env(mut self, trait_env: TraitEnvId<'db>) -> Self { + self.trait_env = Some(trait_env); + self + } + + /// Adds the partial imported data surface visible to this body. + pub fn with_partial_data(mut self, partial_data: Vec<(String, Vec)>) -> Self { + self.partial_data = partial_data; + self + } + + /// Adds the pre-typecheck desugar facts used for this body inference. + pub fn with_pre_typeck_desugar( + mut self, + pre_typeck_desugar: Vec>, + ) -> Self { + self.pre_typeck_desugar = pre_typeck_desugar; + self + } +} diff --git a/crates/hir-ty/src/infer/obligations.rs b/crates/hir-ty/src/infer/obligations.rs new file mode 100644 index 00000000..2e1097e7 --- /dev/null +++ b/crates/hir-ty/src/infer/obligations.rs @@ -0,0 +1,1008 @@ +use super::*; + +pub(super) fn infer_ty_has_comptime_wrapper<'db>(ty: &InferTy<'db>) -> bool { + matches!(ty, InferTy::Comptime(_)) +} + +pub(super) fn ty_requires_comptime<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Comptime(_) => true, + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Integer), + args, + } => args.is_empty(), + _ => false, + } +} + +struct CanonicalizedPending<'db> { + pred: Pred<'db>, + allowed_vars: Vec, + goal_vars: FxHashMap>, +} + +pub(super) struct ObligationCanonicalizer<'a, 'db> { + db: &'db dyn Db, + engine: &'a mut InferTable<'db>, + next: u32, + vars: FxHashMap, u32>, + goal_vars: FxHashMap>, +} + +impl<'a, 'db> ObligationCanonicalizer<'a, 'db> { + pub(super) fn new( + db: &'db dyn Db, + engine: &'a mut InferTable<'db>, + rigid_binders: u32, + ) -> Self { + Self { + db, + engine, + next: rigid_binders, + vars: FxHashMap::default(), + goal_vars: FxHashMap::default(), + } + } + + pub(super) fn ty(&mut self, ty: InferTy<'db>) -> Ty<'db> { + match self.engine.resolve(ty) { + InferTy::Error => Ty::error(self.db), + InferTy::Unknown => Ty::unknown(self.db), + InferTy::Var(var) => { + let root = self.engine.table.find(var); + let index = *self.vars.entry(root).or_insert_with(|| { + let index = self.next; + self.next += 1; + self.goal_vars.insert(index, root); + index + }); + Ty::bound(self.db, index) + } + InferTy::BoundVar(index) => Ty::bound(self.db, index), + InferTy::Named { ctor, args } => Ty::named( + self.db, + ctor, + args.into_iter().map(|arg| self.ty(arg)).collect(), + ), + InferTy::Function { params, ret } => Ty::function( + self.db, + params.into_iter().map(|param| self.ty(param)).collect(), + self.ty(*ret), + ), + InferTy::Tuple(elems) => Ty::tuple( + self.db, + elems.into_iter().map(|elem| self.ty(elem)).collect(), + ), + InferTy::Comptime(inner) => Ty::comptime(self.db, self.ty(*inner)), + } + } + + pub(super) fn allowed_vars(&self) -> Vec { + let mut vars = self.goal_vars.keys().copied().collect::>(); + vars.sort_unstable(); + vars + } +} + +pub(super) struct InferredSchemeGeneralizer<'a, 'db> { + db: &'db dyn Db, + engine: &'a mut InferTable<'db>, + base_binders: u32, + next: u32, + vars: FxHashMap, u32>, +} + +impl<'a, 'db> InferredSchemeGeneralizer<'a, 'db> { + pub(super) fn new(db: &'db dyn Db, engine: &'a mut InferTable<'db>, base_binders: u32) -> Self { + Self { + db, + engine, + base_binders, + next: 0, + vars: FxHashMap::default(), + } + } + + pub(super) fn ty(&mut self, ty: InferTy<'db>) -> Ty<'db> { + match self.engine.resolve(ty) { + InferTy::Error => Ty::error(self.db), + InferTy::Unknown => Ty::unknown(self.db), + InferTy::Var(var) => { + let root = self.engine.table.find(var); + let index = *self.vars.entry(root).or_insert_with(|| { + let index = self.base_binders + self.next; + self.next += 1; + index + }); + Ty::bound(self.db, index) + } + InferTy::BoundVar(index) => Ty::bound(self.db, index), + InferTy::Named { ctor, args } => Ty::named( + self.db, + ctor, + args.into_iter().map(|arg| self.ty(arg)).collect(), + ), + InferTy::Function { params, ret } => Ty::function( + self.db, + params.into_iter().map(|param| self.ty(param)).collect(), + self.ty(*ret), + ), + InferTy::Tuple(elems) => Ty::tuple( + self.db, + elems.into_iter().map(|elem| self.ty(elem)).collect(), + ), + InferTy::Comptime(inner) => Ty::comptime(self.db, self.ty(*inner)), + } + } + + pub(super) fn binder_count(&self) -> u32 { + self.base_binders + self.next + } +} + +#[derive(Default)] +pub(super) struct ObligationSolveOutput<'db> { + pub(super) evidence: Vec>, + pub(super) call_site_evidence: Vec>, + pub(super) diagnostics: Vec, +} + +/// Outcome of one attempt at a pending obligation. +enum ObligationAttempt<'db> { + /// Evidence was recorded and the solver substitution (or closure + /// unification) may have advanced the inference state. + Solved, + /// Nothing further to do: the obligation was skipped (poisoned or + /// error-tainted) or a diagnostic was emitted for a goal that can no + /// longer improve. + Settled, + /// The goal failed but still mentions inference variables; retry after + /// other obligations make progress. + Deferred(FxHashMap, InferTy<'db>>), +} + +pub(super) fn deferred_obligations_affected_by<'db>( + engine: &mut InferTable<'db>, + deferred: &FxHashMap, InferTy<'db>>>, +) -> Vec { + let mut affected = deferred + .iter() + .filter_map(|(index, snapshot)| { + snapshot + .iter() + .any(|(var, previous)| engine.resolve(InferTy::Var(*var)) != *previous) + .then_some(*index) + }) + .collect::>(); + affected.sort_unstable(); + affected +} + +fn record_obligation_evidence<'db>( + index: usize, + pending: &PendingObligation<'db>, + proof: Evidence<'db>, + evidence: &mut Vec>, + call_site_evidence: &mut Vec>, +) { + evidence.push(ObligationEvidence { + obligation: index, + evidence: proof.clone(), + }); + if let ObligationSource::CallSite { + body, + call_expr, + callee_expr, + callee, + } = &pending.source + { + call_site_evidence.push(CallSiteEvidence { + body: *body, + call_expr: *call_expr, + callee_expr: *callee_expr, + callee: callee.clone(), + obligation: index, + evidence: proof, + }); + } +} + +fn apply_solver_ty_subst<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + subst: &FxHashMap>, +) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => subst + .get(&var.index) + .copied() + .map(|ty| apply_solver_ty_subst(db, ty, subst)) + .unwrap_or(ty), + TyKind::Named { ctor, args } => Ty::named( + db, + *ctor, + args.iter() + .map(|arg| apply_solver_ty_subst(db, *arg, subst)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| apply_solver_ty_subst(db, *param, subst)) + .collect(), + apply_solver_ty_subst(db, *ret, subst), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| apply_solver_ty_subst(db, *elem, subst)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, apply_solver_ty_subst(db, *inner, subst)), + TyKind::Error | TyKind::Unknown => ty, + } +} + +impl<'db> InferCtx<'db> { + pub(super) fn solve_pending_obligations( + &mut self, + trait_env: TraitEnvId<'db>, + ) -> ObligationSolveOutput<'db> { + let mut evidence = Vec::new(); + let mut call_site_evidence = Vec::new(); + let mut diagnostics: Vec<(usize, TypeckDiagnostic)> = Vec::new(); + + let pending = self.pending.clone(); + let mut deferred = FxHashMap::, InferTy<'db>>>::default(); + let mut scheduled: Vec = (0..pending.len()).collect(); + + // Improvement rounds, mirroring the reference's `toHnfs` fixpoint: + // solving one obligation can pin goal metavariables of a sibling via + // class-argument unification (improvement). A deferred obligation + // records each inference-variable handle and its resolved value, then + // is retried only when that snapshot changes. Keeping the original + // handles is important: ena may replace their union root, but resolving + // an old handle still follows the union to the current representative. + // Ground and unchanged goals therefore avoid another normalization, + // interning, and solver lookup. Each continuing round resolves at + // least one obligation, bounding the loop by `pending.len()` rounds. + loop { + let mut progress = false; + for index in std::mem::take(&mut scheduled) { + match self.attempt_obligation( + trait_env, + index, + &pending[index], + true, + &mut evidence, + &mut call_site_evidence, + &mut diagnostics, + ) { + ObligationAttempt::Solved => { + deferred.remove(&index); + progress = true; + } + ObligationAttempt::Settled => { + deferred.remove(&index); + } + ObligationAttempt::Deferred(dependencies) => { + deferred.insert(index, dependencies); + } + } + } + if !progress || deferred.is_empty() { + break; + } + scheduled = deferred_obligations_affected_by(&mut self.engine, &deferred); + if scheduled.is_empty() { + break; + } + } + + let mut unresolved = deferred.into_keys().collect::>(); + unresolved.sort_unstable(); + + self.default_integer_literals_with_non_int_obligations(&pending, &unresolved); + + // Final phase: no further improvement is possible, so report the + // remaining deferred obligations exactly as the single-pass solver + // did, in ascending obligation order. + for index in unresolved { + self.attempt_obligation( + trait_env, + index, + &pending[index], + false, + &mut evidence, + &mut call_site_evidence, + &mut diagnostics, + ); + } + + // Consumers key on the stored obligation index; keep the outputs + // index-sorted so round interleaving cannot perturb downstream order. + evidence.sort_by_key(|entry| entry.obligation); + call_site_evidence.sort_by_key(|entry| entry.obligation); + diagnostics.sort_by_key(|(index, _)| *index); + + ObligationSolveOutput { + evidence, + call_site_evidence, + diagnostics: diagnostics + .into_iter() + .map(|(_, diagnostic)| diagnostic) + .collect(), + } + } + + fn default_integer_literals_with_non_int_obligations( + &mut self, + pending: &[PendingObligation<'db>], + unresolved: &[usize], + ) { + let mut constrained_vars = FxHashSet::default(); + for &index in unresolved { + let obligation = &pending[index]; + if obligation.class == ClassId::Builtin(BuiltinClassId::Int) { + continue; + } + self.collect_infer_vars(obligation.main.clone(), &mut constrained_vars); + for arg in &obligation.args { + self.collect_infer_vars(arg.clone(), &mut constrained_vars); + } + } + if constrained_vars.is_empty() { + return; + } + + let word = self.word(); + for &index in unresolved { + let obligation = &pending[index]; + if obligation.class != ClassId::Builtin(BuiltinClassId::Int) + || !obligation.args.is_empty() + || !matches!( + obligation.source, + ObligationSource::IntegerLiteral { .. } + | ObligationSource::IntegerLiteralPattern { .. } + ) + { + continue; + } + let mut vars = FxHashSet::default(); + self.collect_infer_vars(obligation.main.clone(), &mut vars); + if vars.iter().any(|var| constrained_vars.contains(var)) { + self.unify(obligation.main.clone(), word.clone()); + } + } + } + + /// Attempts a single pending obligation. + /// + /// When `defer_unsolved` is true (improvement rounds), failures on goals + /// that still mention inference variables return + /// [`ObligationAttempt::Deferred`] without reporting; otherwise (final + /// phase) failures emit the same diagnostics as the historical + /// single-pass solver. + #[allow(clippy::too_many_arguments)] + fn attempt_obligation( + &mut self, + trait_env: TraitEnvId<'db>, + index: usize, + pending: &PendingObligation<'db>, + defer_unsolved: bool, + evidence: &mut Vec>, + call_site_evidence: &mut Vec>, + diagnostics: &mut Vec<(usize, TypeckDiagnostic)>, + ) -> ObligationAttempt<'db> { + // Re-checked on every attempt: poisoning can grow as other + // obligations unify error types into this obligation's source. + if self.obligation_source_poisoned(&pending.source) + || self.pending_obligation_has_error(pending) + { + return ObligationAttempt::Settled; + } + if self.open_integer_obligation(pending) { + return if defer_unsolved { + let vars = self.pending_obligation_infer_vars(pending); + ObligationAttempt::Deferred(self.snapshot_infer_vars(vars)) + } else { + ObligationAttempt::Settled + }; + } + if let Some(proof) = self.solve_local_closure_obligation(pending) { + record_obligation_evidence(index, pending, proof, evidence, call_site_evidence); + return ObligationAttempt::Solved; + } + // Re-canonicalized on every attempt: the goal resolves through the + // inference engine, so substitutions applied by other obligations + // refine it between rounds. + let pred = self.pending_obligation_pred(pending); + if matches!(pred.pred.kind(self.db), PredKind::Error) { + return ObligationAttempt::Settled; + } + let can_improve = defer_unsolved && !pred.allowed_vars.is_empty(); + let dependencies = self.snapshot_infer_vars(pred.goal_vars.values().copied()); + let span = self.obligation_source_label_span(&pending.source); + let report = solve_report( + self.db, + trait_env, + canonical_goal_with_allowed(self.db, pred.pred, pred.allowed_vars.clone()), + ); + if report.exhausted { + if can_improve { + return ObligationAttempt::Deferred(dependencies); + } + let pred_text = self.display_pred(pred.pred); + diagnostics.push(( + index, + TypeckDiagnostic::SolverFuelExhausted { + span, + pred: pred_text, + }, + )); + return ObligationAttempt::Settled; + } + match report.solution { + Solution::Unique { + subst, + evidence: proof, + } => { + if !solver_answer_is_closed_over_goal(self.db, pred.pred, trait_env, &subst, &proof) + { + if can_improve { + return ObligationAttempt::Deferred(dependencies); + } + let pred_text = self.display_pred(pred.pred); + diagnostics.push(( + index, + TypeckDiagnostic::AmbiguousConstraint { + span, + pred: pred_text, + candidates: vec![ + "the matching proof leaves existential type variables unresolved" + .to_owned(), + ], + }, + )); + return ObligationAttempt::Settled; + } + self.apply_solver_substitution(&pred.goal_vars, &subst); + record_obligation_evidence(index, pending, proof, evidence, call_site_evidence); + ObligationAttempt::Solved + } + Solution::Ambiguous { candidates } => { + if can_improve { + return ObligationAttempt::Deferred(dependencies); + } + let pred_text = self.display_pred(pred.pred); + diagnostics.push(( + index, + TypeckDiagnostic::AmbiguousConstraint { + span, + pred: pred_text, + candidates: vec![format!("{} matching candidates", candidates.len())], + }, + )); + ObligationAttempt::Settled + } + Solution::NoSolution => { + if can_improve { + return ObligationAttempt::Deferred(dependencies); + } + if !pred.allowed_vars.is_empty() { + if !self.reported_ambiguous_constraint { + self.reported_ambiguous_constraint = true; + let pred_text = self.display_pred(pred.pred); + let root_ty = self.root_infer_ty(); + let root_ty = self.display_infer_ty(root_ty); + diagnostics.push(( + index, + TypeckDiagnostic::AmbiguousInferredType { + span: self.body_label_span(self.root_body), + scheme: format!("<_> {root_ty} where {pred_text}"), + }, + )); + } + return ObligationAttempt::Settled; + } + let span = self.unsatisfied_constraint_label_span(&pending.source, pred.pred); + let pred_text = self.display_pred(pred.pred); + let diagnostic = self.classify_no_solution(pending).unwrap_or({ + TypeckDiagnostic::UnsatisfiedConstraint { + span, + pred: pred_text, + } + }); + diagnostics.push((index, diagnostic)); + ObligationAttempt::Settled + } + } + } + + fn solve_local_closure_obligation( + &mut self, + pending: &PendingObligation<'db>, + ) -> Option> { + if pending.class != ClassId::Builtin(BuiltinClassId::Invokable) || pending.args.len() != 2 { + return None; + } + let main = self.normalize_aliases(pending.main.clone()); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(main) + else { + return None; + }; + if !args.is_empty() { + return None; + } + let sig = self.closure_sigs.get(&def)?.clone(); + self.unify(pending.args[0].clone(), invokable_arg_infer(sig.params)); + self.unify(pending.args[1].clone(), sig.ret); + let pred = self.pending_obligation_pred(pending).pred; + Some(Evidence::Derived { + kind: DerivedClauseKind::Closure, + pred, + sub_evidence: Vec::new(), + }) + } + + fn pending_obligation_infer_vars( + &mut self, + pending: &PendingObligation<'db>, + ) -> FxHashSet> { + let mut vars = FxHashSet::default(); + self.collect_infer_vars(pending.main.clone(), &mut vars); + for arg in &pending.args { + self.collect_infer_vars(arg.clone(), &mut vars); + } + vars + } + + fn snapshot_infer_vars( + &mut self, + vars: impl IntoIterator>, + ) -> FxHashMap, InferTy<'db>> { + vars.into_iter() + .map(|var| (var, self.engine.resolve(InferTy::Var(var)))) + .collect() + } + + fn classify_no_solution( + &mut self, + pending: &PendingObligation<'db>, + ) -> Option { + if pending.class == ClassId::Builtin(BuiltinClassId::Int) + && pending.args.is_empty() + && self.is_concrete_non_numeric(pending.main.clone()) + { + let actual_ty = self.normalize_aliases(pending.main.clone()); + let actual = self.display_infer_ty(actual_ty); + return match pending.source { + ObligationSource::IntegerLiteral { body, expr } => { + self.poison_expr(body, expr); + let actual = self + .expected_expr_displays + .get(&(body, expr)) + .cloned() + .unwrap_or_else(|| actual.clone()); + Some(TypeckDiagnostic::Mismatch { + span: self.expr_label_span(body, expr), + expected: "numeric".to_owned(), + actual, + }) + } + ObligationSource::IntegerLiteralPattern { body, pat } => { + self.poison_pat(body, pat); + Some(TypeckDiagnostic::Mismatch { + span: self.pat_label_span(body, pat), + expected: "numeric".to_owned(), + actual, + }) + } + _ => None, + }; + } + + if pending.class == ClassId::Builtin(BuiltinClassId::Invokable) + && pending.args.len() == 2 + && self.is_concrete_non_callable(pending.main.clone()) + && let ObligationSource::CallSite { + body, + call_expr, + callee_expr, + .. + } = pending.source + { + self.poison_expr(body, callee_expr); + self.poison_expr(body, call_expr); + let callee_ty = self.normalize_aliases(pending.main.clone()); + let callee = self.display_infer_ty(callee_ty); + return Some(TypeckDiagnostic::NonCallable { + span: self.expr_label_span(body, callee_expr), + callee, + }); + } + + None + } + + fn obligation_source_poisoned(&self, source: &ObligationSource<'db>) -> bool { + match source { + ObligationSource::IntegerLiteral { body, expr } + | ObligationSource::ClassMethod { body, expr } => self.expr_is_poisoned(*body, *expr), + ObligationSource::CallSite { + body, + call_expr, + callee_expr, + .. + } => { + self.expr_is_poisoned(*body, *call_expr) + || self.expr_is_poisoned(*body, *callee_expr) + } + ObligationSource::IntegerLiteralPattern { body, pat } => { + self.pat_is_poisoned(*body, *pat) + } + ObligationSource::Scheme => false, + } + } + + fn pending_obligation_has_error(&mut self, pending: &PendingObligation<'db>) -> bool { + self.infer_ty_contains_error(pending.main.clone()) + || pending + .args + .iter() + .cloned() + .any(|arg| self.infer_ty_contains_error(arg)) + } + + fn open_integer_obligation(&mut self, pending: &PendingObligation<'db>) -> bool { + pending.class == ClassId::Builtin(BuiltinClassId::Int) + && pending.args.is_empty() + && matches!( + self.engine.resolve(pending.main.clone()), + InferTy::Unknown | InferTy::Var(_) + ) + } + + fn infer_ty_contains_error(&mut self, ty: InferTy<'db>) -> bool { + match self.engine.resolve(ty) { + InferTy::Error => true, + InferTy::Named { args, .. } | InferTy::Tuple(args) => args + .into_iter() + .any(|arg| self.infer_ty_contains_error(arg)), + InferTy::Function { params, ret } => { + params + .into_iter() + .any(|param| self.infer_ty_contains_error(param)) + || self.infer_ty_contains_error(*ret) + } + InferTy::Comptime(inner) => self.infer_ty_contains_error(*inner), + InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_) => false, + } + } + + pub(super) fn is_concrete_non_numeric(&mut self, ty: InferTy<'db>) -> bool { + let ty = self.normalize_aliases(ty); + match self.engine.resolve(ty) { + InferTy::Error | InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_) => false, + InferTy::Comptime(inner) => self.is_concrete_non_numeric(*inner), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Word | crate::BuiltinTyCtor::Integer), + args, + } => !args.is_empty(), + _ => true, + } + } + + fn is_concrete_non_callable(&mut self, ty: InferTy<'db>) -> bool { + if self.callable_sig_for_ty(ty.clone()).is_some() { + return false; + } + let ty = self.normalize_aliases(ty); + !matches!( + self.engine.resolve(ty), + InferTy::Error | InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_) + ) + } + + fn pending_obligation_pred( + &mut self, + pending: &PendingObligation<'db>, + ) -> CanonicalizedPending<'db> { + let main = self.normalize_aliases(pending.main.clone()); + let args = pending + .args + .iter() + .cloned() + .map(|arg| self.normalize_aliases(arg)) + .collect::>(); + let mut canonicalizer = + ObligationCanonicalizer::new(self.db, &mut self.engine, self.root_binder_count); + let main = canonicalizer.ty(main); + let args = args.into_iter().map(|arg| canonicalizer.ty(arg)).collect(); + let allowed_vars = canonicalizer.allowed_vars(); + let goal_vars = canonicalizer.goal_vars; + let pred = self.normalize_pred_aliases(Pred::in_class(self.db, pending.class, main, args)); + CanonicalizedPending { + pred, + allowed_vars, + goal_vars, + } + } + + fn apply_solver_substitution( + &mut self, + goal_vars: &FxHashMap>, + subst: &Substitution<'db>, + ) { + let values = subst.values.iter().copied().collect::>(); + for (solver_var, infer_var) in goal_vars { + let Some(value) = values.get(solver_var).copied() else { + continue; + }; + let value = apply_solver_ty_subst(self.db, value, &values); + if matches!(value.kind(self.db), TyKind::BoundVar(var) if var.index == *solver_var) { + continue; + } + let value = self.infer_from_solver_ty(value, goal_vars); + self.unify(InferTy::Var(*infer_var), value); + } + } + + fn infer_from_solver_ty( + &mut self, + ty: Ty<'db>, + goal_vars: &FxHashMap>, + ) -> InferTy<'db> { + match ty.kind(self.db) { + TyKind::BoundVar(var) => goal_vars + .get(&var.index) + .copied() + .map(InferTy::Var) + .unwrap_or(InferTy::BoundVar(var.index)), + TyKind::Error => InferTy::Error, + TyKind::Unknown => InferTy::Unknown, + TyKind::Named { ctor, args } => InferTy::Named { + ctor: *ctor, + args: args + .iter() + .map(|arg| self.infer_from_solver_ty(*arg, goal_vars)) + .collect(), + }, + TyKind::Function { params, ret } => InferTy::Function { + params: params + .iter() + .map(|param| self.infer_from_solver_ty(*param, goal_vars)) + .collect(), + ret: Box::new(self.infer_from_solver_ty(*ret, goal_vars)), + }, + TyKind::Tuple(elems) => InferTy::Tuple( + elems + .iter() + .map(|elem| self.infer_from_solver_ty(*elem, goal_vars)) + .collect(), + ), + TyKind::Comptime(inner) => { + InferTy::Comptime(Box::new(self.infer_from_solver_ty(*inner, goal_vars))) + } + } + } + + pub(super) fn default_integer_literal_patterns(&mut self) { + let word = self.word(); + for var in self.integer_literal_pattern_vars.clone() { + if matches!(self.engine.resolve(InferTy::Var(var)), InferTy::Var(_)) { + self.unify(InferTy::Var(var), word.clone()); + } + } + } + + pub(super) fn check_ambiguous_integer_literals(&mut self) { + let root_ty = self.root_infer_ty(); + let mut root_vars = FxHashSet::default(); + self.collect_infer_vars(root_ty.clone(), &mut root_vars); + + let mut ambiguous = Vec::new(); + for pending in self.pending.clone() { + if pending.class != ClassId::Builtin(BuiltinClassId::Int) + || !pending.args.is_empty() + || matches!( + pending.source, + ObligationSource::IntegerLiteralPattern { .. } + ) + || self.obligation_source_poisoned(&pending.source) + || self.pending_obligation_has_error(&pending) + { + continue; + } + let mut vars = FxHashSet::default(); + self.collect_infer_vars(pending.main.clone(), &mut vars); + if vars.is_empty() || vars.iter().all(|var| root_vars.contains(var)) { + continue; + } + ambiguous.push(self.display_infer_ty(pending.main)); + } + + ambiguous.sort(); + ambiguous.dedup(); + if ambiguous.is_empty() { + return; + } + + let preds = ambiguous + .into_iter() + .map(|main| format!("{main}: Int")) + .collect::>() + .join(", "); + let scheme = format!("<_> {} where {preds}", self.display_infer_ty(root_ty)); + self.diagnostics + .push(TypeckDiagnostic::AmbiguousInferredType { + span: self.body_label_span(self.root_body), + scheme, + }); + } + + pub(super) fn check_ambiguous_constructor_results(&mut self) { + let constructor_results = self + .phantom_constructor_results + .iter() + .map(|(key, value)| (*key, value.clone())) + .collect::>(); + for ((body, expr), (ty, phantom_vars)) in constructor_results { + if self.expr_is_poisoned(body, expr) { + continue; + } + let mut unresolved = phantom_vars + .into_iter() + .filter_map(|var| match self.engine.resolve(InferTy::Var(var)) { + InferTy::Var(root) => Some(root), + _ => None, + }) + .collect::>(); + unresolved.sort_by_key(|var| var.index()); + unresolved.dedup(); + if unresolved.is_empty() { + continue; + } + + let vars = unresolved + .into_iter() + .map(|var| self.display_infer_ty(InferTy::Var(var))) + .collect::>(); + let result = self.display_infer_ty(ty); + self.diagnostics + .push(TypeckDiagnostic::AmbiguousInferredType { + span: self.expr_label_span(body, expr), + scheme: format!( + "constructor result {result} leaves {} unconstrained", + vars.join(", ") + ), + }); + return; + } + } + + /// Records only constructor result variables that cannot be learned from + /// the constructor payload. Normal constructors such as `Box(a) = Box(a)` + /// share their result variable with a parameter and therefore do not enter + /// the ambiguity check. + pub(super) fn record_phantom_constructor_result( + &mut self, + body: FuncBody<'db>, + expr: Id>, + ctor_ty: InferTy<'db>, + ) { + let resolved = self.engine.resolve(ctor_ty); + let (params, ret) = match resolved { + InferTy::Function { params, ret } => (params, *ret), + result => (Vec::new(), result), + }; + let mut param_vars = FxHashSet::default(); + for param in params { + self.collect_infer_vars(param, &mut param_vars); + } + let mut result_vars = FxHashSet::default(); + self.collect_infer_vars(ret.clone(), &mut result_vars); + let mut phantom_vars = result_vars + .into_iter() + .filter(|var| !param_vars.contains(var)) + .collect::>(); + phantom_vars.sort_by_key(|var| var.index()); + if !phantom_vars.is_empty() { + self.phantom_constructor_results + .insert((body, expr), (ret, phantom_vars)); + } + } + + pub(super) fn default_root_integer_literals(&mut self) { + let root_ty = self.root_infer_ty(); + let mut root_vars = FxHashSet::default(); + self.collect_infer_vars(root_ty, &mut root_vars); + if root_vars.is_empty() { + return; + } + + let word = self.word(); + for pending in self.pending.clone() { + if pending.class != ClassId::Builtin(BuiltinClassId::Int) + || !pending.args.is_empty() + || self.obligation_source_poisoned(&pending.source) + || self.pending_obligation_has_error(&pending) + { + continue; + } + let mut vars = FxHashSet::default(); + self.collect_infer_vars(pending.main.clone(), &mut vars); + if !vars.is_empty() && vars.iter().all(|var| root_vars.contains(var)) { + self.unify(pending.main.clone(), word.clone()); + } + } + } + + fn root_infer_ty(&mut self) -> InferTy<'db> { + let params = (0..self.root_param_count) + .map(|index| { + self.param_tys + .get(&(self.root_body, index as u32)) + .cloned() + .unwrap_or(InferTy::Error) + }) + .collect::>(); + let ret = self.return_stack.first().cloned().unwrap_or(InferTy::Error); + InferTy::Function { + params, + ret: Box::new(ret), + } + } + + fn collect_infer_vars(&mut self, ty: InferTy<'db>, out: &mut FxHashSet>) { + match self.engine.resolve(ty) { + InferTy::Var(var) => { + out.insert(var); + } + InferTy::Named { args, .. } | InferTy::Tuple(args) => { + for arg in args { + self.collect_infer_vars(arg, out); + } + } + InferTy::Function { params, ret } => { + for param in params { + self.collect_infer_vars(param, out); + } + self.collect_infer_vars(*ret, out); + } + InferTy::Comptime(inner) => self.collect_infer_vars(*inner, out), + InferTy::Error | InferTy::Unknown | InferTy::BoundVar(_) => {} + } + } +} + +fn solver_answer_is_closed_over_goal<'db>( + db: &'db dyn Db, + goal: Pred<'db>, + trait_env: TraitEnvId<'db>, + subst: &Substitution<'db>, + evidence: &Evidence<'db>, +) -> bool { + let mut goal_vars = FxHashSet::default(); + collect_pred_vars(db, goal, &mut goal_vars); + for given in trait_env.local_givens(db) { + collect_pred_vars(db, *given, &mut goal_vars); + } + + let mut answer_vars = FxHashSet::default(); + for (_, ty) in &subst.values { + collect_ty_vars(db, *ty, &mut answer_vars); + } + collect_evidence_vars(db, evidence, &mut answer_vars); + + answer_vars.is_subset(&goal_vars) +} diff --git a/crates/hir-ty/src/infer/origin.rs b/crates/hir-ty/src/infer/origin.rs new file mode 100644 index 00000000..a25faf39 --- /dev/null +++ b/crates/hir-ty/src/infer/origin.rs @@ -0,0 +1,46 @@ +use super::*; + +pub(super) struct DiagnosticSourceMap<'a, 'db> { + db: &'db dyn Db, + view: BodyDesugarView<'a, 'db>, +} + +impl<'a, 'db> DiagnosticSourceMap<'a, 'db> { + pub(super) fn new(db: &'db dyn Db, plans: &'a [BodyPreTypeckDesugarPlan<'db>]) -> Self { + Self { + db, + view: BodyDesugarView::new(plans), + } + } + + pub(super) fn label_span(&self, span: Span<'db>) -> LabelSpan { + LabelSpan::from_span(self.db, span) + } + + pub(super) fn stmt_label_span(&self, body: FuncBody<'db>, stmt: Id>) -> LabelSpan { + let fallback = body.stmts(self.db).get(stmt).span(self.db); + label_span_for_origin(self.db, self.view.stmt_origin(body, stmt), fallback) + } + + pub(super) fn expr_label_span(&self, body: FuncBody<'db>, expr: Id>) -> LabelSpan { + let fallback = body.exprs(self.db).get(expr).span(self.db); + label_span_for_origin(self.db, self.view.expr_origin(body, expr), fallback) + } + + pub(super) fn pat_label_span(&self, body: FuncBody<'db>, pat: Id>) -> LabelSpan { + let fallback = body.pats(self.db).get(pat).span(self.db); + label_span_for_origin(self.db, self.view.pat_origin(body, pat), fallback) + } + + pub(super) fn type_label_span(&self, ty: TypeRef<'db>) -> LabelSpan { + label_span_for_origin(self.db, self.view.type_origin(ty), ty.span(self.db)) + } +} + +pub(super) fn label_span_for_origin<'db>( + db: &'db dyn Db, + origin: Option>, + fallback: Span<'db>, +) -> LabelSpan { + LabelSpan::from_span(db, origin.map(|origin| origin.span).unwrap_or(fallback)) +} diff --git a/crates/hir-ty/src/infer/pattern.rs b/crates/hir-ty/src/infer/pattern.rs new file mode 100644 index 00000000..66f7d82a --- /dev/null +++ b/crates/hir-ty/src/infer/pattern.rs @@ -0,0 +1,1121 @@ +use super::*; + +impl<'db> InferCtx<'db> { + pub(super) fn infer_pat_expected( + &mut self, + body: FuncBody<'db>, + pat_id: Id>, + expected: Option>, + ) -> InferTy<'db> { + let pat = body.pats(self.db).get(pat_id); + let mut ty = match &pat.kind { + PatKind::Wildcard => expected.clone().unwrap_or_else(|| self.engine.fresh_var()), + PatKind::Var(name) => match self.pat_resolutions.get(&(body, pat_id)).cloned() { + // Bool constructor patterns have an early unit-sum view, while + // nameres remains the source of truth for whether the spelling + // is actually the builtin constructor. + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + ctor @ (hir_nameres::BuiltinCtor::True | hir_nameres::BuiltinCtor::False), + ))) => { + if let Some(unit_sum) = self.bool_pat_unit_sum(body, pat_id) { + debug_assert_eq!( + unit_sum.value, + matches!(ctor, hir_nameres::BuiltinCtor::True) + ); + } + self.infer_ctor_pat(body, pat_id, &[], expected.clone()) + } + // Unqualified constructor misuse already reported by nameres + // follows nullary constructor-pattern inference instead of + // binding a fresh local. + Some(hir_nameres::Resolution::Ctor { .. } | hir_nameres::Resolution::Err) => { + self.infer_ctor_pat(body, pat_id, &[], expected.clone()) + } + _ => { + let ty = expected.clone().unwrap_or_else(|| self.engine.fresh_var()); + self.pat_tys_for_locals.insert((body, pat_id), ty.clone()); + self.add_sail_local((*name.atom()).text(self.db).to_owned(), ty.clone()); + ty + } + }, + PatKind::Lit(lit) => self.infer_lit_pat(body, pat_id, lit, expected.clone()), + PatKind::Tuple { elems } => self.infer_tuple_pat(body, pat_id, elems, expected.clone()), + PatKind::Ctor { args, .. } => self.infer_ctor_pat(body, pat_id, args, expected.clone()), + PatKind::ComptimeLabel { expr, .. } => { + let label_ty = self.infer_expr_expected(body, *expr, expected.clone()); + if !self.is_numeric_or_open(label_ty.clone()) { + let actual = self.display_infer_ty(label_ty); + self.emit_expr_error( + body, + *expr, + TypeckDiagnostic::Mismatch { + span: self.expr_label_span(body, *expr), + expected: "numeric".to_owned(), + actual, + }, + ); + } + self.comptime_obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::PatternLabel { pat: pat_id }, + }); + expected.clone().unwrap_or_else(|| self.engine.fresh_var()) + } + PatKind::Error => InferTy::Error, + }; + if let Some(expected) = expected + && !self.unify_pat(body, pat_id, expected, ty.clone()) + { + ty = InferTy::Error; + } + if self.pat_is_poisoned(body, pat_id) { + ty = InferTy::Error; + } + self.pat_tys.push((body, pat_id, ty.clone())); + ty + } + + fn infer_lit_pat( + &mut self, + body: FuncBody<'db>, + pat: Id>, + lit: &LitKind, + expected: Option>, + ) -> InferTy<'db> { + match lit { + LitKind::Number(_) | LitKind::Hex(_) => { + let vid = self.engine.fresh_vid(); + let ty = InferTy::Var(vid); + self.integer_literal_pattern_vars.push(vid); + self.pending.push(PendingObligation { + class: ClassId::Builtin(BuiltinClassId::Int), + main: ty.clone(), + args: Vec::new(), + source: ObligationSource::IntegerLiteralPattern { body, pat }, + }); + if let Some(expected) = expected { + if let Some(numeric_expected) = + self.numeric_literal_pattern_expected(expected.clone()) + { + // `comptime` is a staging property of the scrutinee, + // not part of the numeric literal itself. Constrain the + // literal's `Int` variable with the wrapped numeric type + // while retaining the original expected pattern type. + self.unify_pat(body, pat, numeric_expected, ty); + expected + } else { + let actual = self.display_infer_ty(expected.clone()); + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::Mismatch { + span: self.pat_label_span(body, pat), + expected: "numeric".to_owned(), + actual, + }, + ); + InferTy::Error + } + } else { + ty + } + } + LitKind::String(_) => expected + .and_then(|expected| self.expected_string_lit_ty(expected)) + .unwrap_or_else(|| self.string()), + LitKind::Error => InferTy::Error, + } + } + + fn numeric_literal_pattern_expected(&mut self, expected: InferTy<'db>) -> Option> { + let expected = self.normalize_aliases(expected); + match self.engine.resolve(expected) { + InferTy::Comptime(inner) => self.numeric_literal_pattern_expected(*inner), + ty @ (InferTy::Error | InferTy::Unknown | InferTy::Var(_)) => Some(ty), + ref ty @ InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word | BuiltinTyCtor::Integer), + ref args, + } if args.is_empty() => Some(ty.clone()), + InferTy::Named { .. } + | InferTy::Function { .. } + | InferTy::Tuple(_) + | InferTy::BoundVar(_) => None, + } + } + + pub(super) fn infer_resolution( + &mut self, + body: FuncBody<'db>, + expr: Id>, + resolution: hir_nameres::Resolution<'db>, + ) -> InferTy<'db> { + self.infer_resolution_with_source(body, expr, resolution, None, ValuePosition::Value) + } + + pub(super) fn infer_resolution_with_source( + &mut self, + body: FuncBody<'db>, + expr: Id>, + resolution: hir_nameres::Resolution<'db>, + source: Option>, + position: ValuePosition, + ) -> InferTy<'db> { + match resolution { + hir_nameres::Resolution::Param(param) => { + self.param_ty(param.body, param.index.as_u32()) + } + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Let { body, stmt }) => { + self.let_ty(body, stmt) + } + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::Pattern { body, pat }) => { + self.pattern_local_ty(body, pat) + } + hir_nameres::Resolution::Builtin(kind) => match kind { + hir_nameres::BuiltinKind::Constructor(ctor) => { + if matches!( + ctor, + hir_nameres::BuiltinCtor::True | hir_nameres::BuiltinCtor::False + ) && let Some(unit_sum) = self.bool_expr_unit_sum(body, expr) + { + debug_assert_eq!( + unit_sum.value, + matches!(ctor, hir_nameres::BuiltinCtor::True) + ); + } + let kind = hir_nameres::BuiltinKind::Constructor(ctor); + if let Some(scheme) = builtin_scheme(self.db, kind) { + let instantiated = self.engine.instantiate_scheme_with_source( + scheme, + source.unwrap_or(ObligationSource::Scheme), + ); + self.accept_instantiated(instantiated) + } else { + InferTy::Error + } + } + hir_nameres::BuiltinKind::Function(_) + | hir_nameres::BuiltinKind::ClassMethod(_) => { + if let Some(scheme) = builtin_scheme(self.db, kind) { + let source = source.unwrap_or(match kind { + hir_nameres::BuiltinKind::ClassMethod(_) => { + ObligationSource::ClassMethod { body, expr } + } + _ => ObligationSource::Scheme, + }); + let instantiated = + self.engine.instantiate_scheme_with_source(scheme, source); + self.accept_instantiated(instantiated) + } else { + InferTy::Error + } + } + hir_nameres::BuiltinKind::Type(_) => { + self.namespace_as_value(body, expr, ValueNamespace::Type, position) + } + hir_nameres::BuiltinKind::Class(_) => { + self.namespace_as_value(body, expr, ValueNamespace::Class, position) + } + }, + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => self.instantiate_function(def, source.unwrap_or(ObligationSource::Scheme)), + hir_nameres::Resolution::Field(field) => self.instantiate_field_read( + body, + expr, + field, + source.unwrap_or(ObligationSource::Scheme), + ), + hir_nameres::Resolution::Ctor { ty, index } => self.instantiate_adt_ctor_value( + body, + expr, + ty, + index, + source.unwrap_or(ObligationSource::Scheme), + ), + hir_nameres::Resolution::ClassMethod { class, name } => self.instantiate_class_method( + class, + &name, + source.unwrap_or(ObligationSource::ClassMethod { body, expr }), + ), + hir_nameres::Resolution::Err => InferTy::Error, + hir_nameres::Resolution::Def { kind, .. } => match kind { + hir_nameres::DefResolutionKind::Function => unreachable!("handled above"), + hir_nameres::DefResolutionKind::Adt + | hir_nameres::DefResolutionKind::TypeAlias + | hir_nameres::DefResolutionKind::ValueType + | hir_nameres::DefResolutionKind::Contract + | hir_nameres::DefResolutionKind::Instance => { + self.namespace_as_value(body, expr, ValueNamespace::Type, position) + } + hir_nameres::DefResolutionKind::Class => { + self.namespace_as_value(body, expr, ValueNamespace::Class, position) + } + }, + hir_nameres::Resolution::Module(_) => { + self.namespace_as_value(body, expr, ValueNamespace::Module, position) + } + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::TypeVar(_)) => { + self.namespace_as_value(body, expr, ValueNamespace::TypeVariable, position) + } + hir_nameres::Resolution::DotCtorDeferred => InferTy::Error, + } + } + + fn namespace_as_value( + &mut self, + body: FuncBody<'db>, + expr: Id>, + namespace: ValueNamespace, + position: ValuePosition, + ) -> InferTy<'db> { + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::NamespaceAsValue { + span: self.expr_label_span(body, expr), + name: self.expr_display_name(body, expr), + namespace, + position, + }, + ); + InferTy::Error + } + + fn expr_display_name(&self, body: FuncBody<'db>, expr: Id>) -> String { + match &body.exprs(self.db).get(expr).kind { + ExprKind::Ident(name) => (*name.atom()).text(self.db).to_owned(), + ExprKind::Field { base, field } => { + format!( + "{}.{}", + self.expr_display_name(body, *base), + (*field.atom()).text(self.db) + ) + } + ExprKind::DotCtor { name, .. } => format!(".{}", (*name.atom()).text(self.db)), + _ => "expression".to_owned(), + } + } + + pub(super) fn accept_instantiated(&mut self, instantiated: Instantiated<'db>) -> InferTy<'db> { + let has_equality_errors = !instantiated.equality_errors.is_empty(); + for equality_error in instantiated.equality_errors { + let span = self.obligation_source_label_span(&equality_error.source); + self.diagnostics.push(equality_error.error.diagnostic( + &mut self.engine, + span, + &self.type_var_names, + )); + } + self.pending.extend(instantiated.obligations); + if has_equality_errors { + InferTy::Error + } else { + instantiated.ty + } + } + + fn instantiate_function( + &mut self, + def: DefId<'db>, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + if let Some(scheme) = self.lookup_function_scheme(def) { + let instantiated = self.engine.instantiate_scheme_with_source(scheme, source); + self.accept_instantiated(instantiated) + } else { + self.engine.fresh_var() + } + } + + pub(super) fn instantiate_field( + &mut self, + field: hir_nameres::FieldId<'db>, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + if let Some(scheme) = self.lookup_field_scheme(field) { + let instantiated = self.engine.instantiate_scheme_with_source(scheme, source); + self.accept_instantiated(instantiated) + } else { + self.engine.fresh_var() + } + } + + pub(super) fn instantiate_adt_ctor( + &mut self, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + if let Some(scheme) = self.lookup_adt_ctor_scheme(ty, index) { + let instantiated = self.engine.instantiate_scheme_with_source(scheme, source); + self.accept_instantiated(instantiated) + } else { + self.engine.fresh_var() + } + } + + fn instantiate_adt_ctor_value( + &mut self, + body: FuncBody<'db>, + expr: Id>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + let ctor_ty = self.instantiate_adt_ctor(ty, index, source); + self.record_phantom_constructor_result(body, expr, ctor_ty.clone()); + match self.engine.resolve(ctor_ty.clone()) { + InferTy::Function { params, ret } if params.is_empty() => *ret, + _ => ctor_ty, + } + } + + pub(super) fn instantiate_class_method( + &mut self, + class: DefId<'db>, + name: &str, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + if let Some(scheme) = self.lookup_class_method_scheme(class, name) { + let instantiated = self.engine.instantiate_scheme_with_source(scheme, source); + self.accept_instantiated(instantiated) + } else { + self.engine.fresh_var() + } + } + + fn lookup_function_scheme(&self, def: DefId<'db>) -> Option> { + if let Some(entry_module) = self.entry_module { + function_scheme_for_entry(self.db, entry_module, def) + } else { + function_scheme_in_hir_module(self.db, self.module, def) + } + } + + fn lookup_field_scheme(&self, field: hir_nameres::FieldId<'db>) -> Option> { + if let Some(entry_module) = self.entry_module { + field_scheme_for_entry(self.db, entry_module, field) + } else { + field_scheme_in_hir_module(self.db, self.module, field) + } + } + + pub(super) fn lookup_adt_ctor_scheme( + &self, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, + ) -> Option> { + if let Some(entry_module) = self.entry_module { + adt_ctor_scheme_for_entry(self.db, entry_module, ty, index) + } else { + adt_ctor_scheme_in_hir_module(self.db, self.module, ty, index) + } + } + + pub(super) fn lookup_adt_field_index( + &self, + ty: DefId<'db>, + name: &str, + ) -> Option<(hir_nameres::CtorIndex, u32)> { + if let Some(entry_module) = self.entry_module { + adt_field_index_for_entry(self.db, entry_module, ty, name) + } else { + adt_field_index_in_hir_module(self.db, self.module, ty, name) + } + } + + fn lookup_class_method_scheme(&self, class: DefId<'db>, name: &str) -> Option> { + if let Some(entry_module) = self.entry_module { + class_method_scheme_for_entry(self.db, entry_module, class, name.to_owned()) + } else { + class_method_scheme_in_hir_module(self.db, self.module, class, name.to_owned()) + } + } + + pub(super) fn infer_dot_ctor_expr( + &mut self, + body: FuncBody<'db>, + expr: Id>, + name: &str, + args: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let Some(expected) = expected else { + for arg in args { + self.infer_expr(body, *arg); + } + self.shorthand_ctor_diag( + self.expr_label_span(body, expr), + name, + "cannot resolve without expected constructor type".to_owned(), + ); + return InferTy::Error; + }; + match self.ctor_for_expected(name, expected.clone()) { + DotCtorLookup::Match { ty, callee } => { + self.apply_ctor_expr_scheme(body, expr, ty, args, expected, Some(callee)) + } + DotCtorLookup::NoExpected => { + for arg in args { + self.infer_expr(body, *arg); + } + self.shorthand_ctor_diag( + self.expr_label_span(body, expr), + name, + "cannot resolve without expected constructor type".to_owned(), + ); + InferTy::Error + } + DotCtorLookup::NoMatch => { + for arg in args { + self.infer_expr(body, *arg); + } + self.shorthand_ctor_diag( + self.expr_label_span(body, expr), + name, + "no matching constructor".to_owned(), + ); + InferTy::Error + } + DotCtorLookup::Ambiguous(candidates) => { + for arg in args { + self.infer_expr(body, *arg); + } + self.shorthand_ctor_diag( + self.expr_label_span(body, expr), + name, + format!("ambiguous candidates: {}", candidates.join(", ")), + ); + InferTy::Error + } + } + } + + pub(super) fn apply_ctor_expr_scheme( + &mut self, + body: FuncBody<'db>, + expr: Id>, + ctor_ty: InferTy<'db>, + args: &[Id>], + expected: InferTy<'db>, + callee: Option>, + ) -> InferTy<'db> { + self.record_phantom_constructor_result(body, expr, ctor_ty.clone()); + match self.engine.resolve(ctor_ty.clone()) { + InferTy::Function { params, ret } => { + if params.len() != args.len() { + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::WrongArity { + span: self.expr_label_span(body, expr), + context: "constructor".to_owned(), + expected: params.len(), + actual: args.len(), + callee: callee.as_ref().and_then(|callee| { + callee_diagnostic_info(self.db, self.entry_module, callee) + }), + }, + ); + for (index, arg) in args.iter().enumerate() { + self.infer_expr_expected(body, *arg, params.get(index).cloned()); + } + return InferTy::Error; + } + let expected_params = args + .iter() + .map(|_| self.engine.fresh_var()) + .collect::>(); + self.unify_expr( + body, + expr, + ctor_ty.clone(), + InferTy::Function { + params: expected_params.clone(), + ret: Box::new(expected.clone()), + }, + ); + self.unify_expr(body, expr, *ret, expected.clone()); + let expected_params = expected_params + .into_iter() + .map(|param| self.engine.resolve(param)) + .collect::>(); + let inferred_args = args + .iter() + .enumerate() + .map(|(index, arg)| { + self.infer_call_arg_expected( + body, + *arg, + expected_params.get(index).cloned(), + callee.as_ref(), + index, + ) + }) + .collect::>(); + self.unify_expr( + body, + expr, + ctor_ty, + InferTy::Function { + params: inferred_args, + ret: Box::new(expected.clone()), + }, + ); + expected + } + non_function => { + if matches!(non_function, InferTy::Error) { + for arg in args { + self.infer_expr(body, *arg); + } + self.poison_expr(body, expr); + return InferTy::Error; + } + if args.is_empty() { + if !self.unify_expr(body, expr, non_function.clone(), expected.clone()) { + return InferTy::Error; + } + } else if !matches!( + non_function, + InferTy::Error | InferTy::Unknown | InferTy::Var(_) + ) { + let callee = self.display_infer_ty(non_function); + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::NonCallable { + span: self.expr_label_span(body, expr), + callee, + }, + ); + for arg in args { + self.infer_expr(body, *arg); + } + return InferTy::Error; + } + for arg in args { + self.infer_expr(body, *arg); + } + expected + } + } + } + + fn ctor_for_expected(&mut self, name: &str, expected: InferTy<'db>) -> DotCtorLookup<'db> { + let expected = self.engine.resolve(expected); + let expected = self.normalize_aliases(expected); + let expected = self.expand_infer_aliases(expected, &mut FxHashSet::default()); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } = &expected + else { + if builtin_ctor_kind_by_name(name).is_some() { + return self.builtin_ctor_for_expected(name, expected); + } + return DotCtorLookup::NoExpected; + }; + let matches = self.lookup_adt_ctor_schemes_by_name(*def, name); + match matches.as_slice() { + [] => DotCtorLookup::NoMatch, + [entry] => { + let instantiated = self.engine.instantiate_scheme(entry.scheme); + let ctor_ty = self.accept_instantiated(instantiated); + DotCtorLookup::Match { + ty: ctor_ty, + callee: CallSiteCallee::AdtCtor { + ty: entry.ty, + index: entry.index, + }, + } + } + entries => DotCtorLookup::Ambiguous( + entries + .iter() + .map(|entry| entry.name.clone()) + .collect::>(), + ), + } + } + + pub(super) fn expand_infer_aliases( + &mut self, + ty: InferTy<'db>, + expanding: &mut FxHashSet>, + ) -> InferTy<'db> { + match self.engine.resolve(ty) { + InferTy::Named { ctor, args } => { + let args = args + .into_iter() + .map(|arg| self.expand_infer_aliases(arg, expanding)) + .collect::>(); + let TyCtor::User(user) = ctor else { + return InferTy::Named { ctor, args }; + }; + if !matches!(user.kind, crate::UserTyCtorKind::Alias) { + return InferTy::Named { ctor, args }; + } + if !expanding.insert(user.def) { + return InferTy::Named { + ctor: TyCtor::User(user), + args, + }; + } + let expanded = self + .lower_type_alias_infer(user.def) + .map(|(body, inherited_type_var_count)| { + let captured_args = (0..inherited_type_var_count) + .map(|index| InferTy::BoundVar(index as u32)) + .chain(args.iter().cloned()) + .collect::>(); + substitute_infer_alias_args(body, &captured_args) + }) + .map(|body| self.expand_infer_aliases(body, expanding)) + .unwrap_or(InferTy::Named { + ctor: TyCtor::User(user), + args, + }); + expanding.remove(&user.def); + expanded + } + InferTy::Function { params, ret } => InferTy::Function { + params: params + .into_iter() + .map(|param| self.expand_infer_aliases(param, expanding)) + .collect(), + ret: Box::new(self.expand_infer_aliases(*ret, expanding)), + }, + InferTy::Tuple(elems) => InferTy::Tuple( + elems + .into_iter() + .map(|elem| self.expand_infer_aliases(elem, expanding)) + .collect(), + ), + InferTy::Comptime(inner) => { + InferTy::Comptime(Box::new(self.expand_infer_aliases(*inner, expanding))) + } + ty @ (InferTy::Error | InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_)) => ty, + } + } + + fn lower_type_alias_infer(&mut self, def: DefId<'db>) -> Option<(InferTy<'db>, usize)> { + if let Some(info) = find_type_alias_info(self.db, self.module, def, &[]) { + let item_resolutions = hir_nameres::resolve_item_types(self.db, self.module); + let lowered = TypeLowering::from_item_resolutions( + self.db, + &item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_type_alias(info.alias) + .ty; + return Some((self.engine.from_ty(lowered), info.inherited_type_var_count)); + } + + let entry = self.entry_module?; + let module = module_for_def(self.db, entry, def)?; + let item_resolutions = item_resolutions_for_module(self.db, module)?; + let hir_module = module_hir(self.db, module)?; + let info = find_type_alias_info(self.db, hir_module, def, &[])?; + let lowered = TypeLowering::from_item_resolutions( + self.db, + &item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_type_alias(info.alias) + .ty; + Some((self.engine.from_ty(lowered), info.inherited_type_var_count)) + } + + fn builtin_ctor_for_expected( + &mut self, + name: &str, + expected: InferTy<'db>, + ) -> DotCtorLookup<'db> { + if matches!( + expected, + InferTy::Error | InferTy::Unknown | InferTy::Var(_) + ) { + return DotCtorLookup::NoExpected; + } + let Some(kind) = builtin_ctor_kind_by_name(name) else { + return DotCtorLookup::NoExpected; + }; + let Some(scheme) = builtin_scheme(self.db, kind) else { + return DotCtorLookup::NoMatch; + }; + let instantiated = self.engine.instantiate_scheme(scheme); + let result = ctor_result_ty(&instantiated.ty); + if self.can_unify(expected, result) { + let ctor_ty = self.accept_instantiated(instantiated); + DotCtorLookup::Match { + ty: ctor_ty, + callee: CallSiteCallee::Builtin(kind), + } + } else { + DotCtorLookup::NoMatch + } + } + + fn lookup_adt_ctor_schemes_by_name( + &self, + ty: DefId<'db>, + name: &str, + ) -> Vec> { + if let Some(entry_module) = self.entry_module { + adt_ctor_schemes_by_name_for_entry(self.db, entry_module, ty, name.to_owned()) + } else { + adt_ctor_schemes_by_name_in_hir_module(self.db, self.module, ty, name.to_owned()) + } + } + + fn shorthand_ctor_diag(&mut self, span: LabelSpan, name: &str, reason: String) { + self.diagnostics + .push(TypeckDiagnostic::ShorthandConstructor { + span, + name: name.to_owned(), + reason, + }); + } + + pub(super) fn infer_tuple_expr( + &mut self, + body: FuncBody<'db>, + expr: Id>, + elems: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let product = self.tuple_expr_product_shape(body, expr, elems); + let elems = product.to_vec(); + let expected_elems = expected.as_ref().and_then(|expected| { + let expected = self.normalize_aliases(expected.clone()); + let expected = self.engine.resolve(expected); + if elems.len() == 1 { + return Some(vec![expected]); + } + match product_elems(&mut self.engine, expected) { + Some(expected_elems) if expected_elems.len() == elems.len() => Some(expected_elems), + Some(expected_elems) => { + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::WrongArity { + span: self.expr_label_span(body, expr), + context: "tuple".to_owned(), + expected: expected_elems.len(), + actual: elems.len(), + callee: None, + }, + ); + Some(expected_elems) + } + _ => None, + } + }); + let inferred = elems + .iter() + .enumerate() + .map(|(index, elem)| { + self.infer_expr_expected( + body, + *elem, + expected_elems + .as_ref() + .and_then(|expected| expected.get(index).cloned()), + ) + }) + .collect(); + if self.expr_is_poisoned(body, expr) { + InferTy::Error + } else { + product_infer_ty(inferred) + } + } + + fn infer_tuple_pat( + &mut self, + body: FuncBody<'db>, + pat: Id>, + elems: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let product = self.tuple_pat_product_shape(body, pat, elems); + let elems = product.to_vec(); + let expected_elems = expected.as_ref().and_then(|expected| { + let expected = self.normalize_aliases(expected.clone()); + let expected = self.engine.resolve(expected); + if elems.len() == 1 { + return Some(vec![expected]); + } + if let Some(expected_elems) = product_elems(&mut self.engine, expected.clone()) { + if expected_elems.len() != elems.len() { + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::WrongArity { + span: self.pat_label_span(body, pat), + context: "tuple pattern".to_owned(), + expected: expected_elems.len(), + actual: elems.len(), + callee: None, + }, + ); + } + return Some(expected_elems); + } + match expected { + InferTy::Var(_) | InferTy::Unknown | InferTy::Error => None, + other => { + let actual = self.display_infer_ty(other); + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::Mismatch { + span: self.pat_label_span(body, pat), + expected: "tuple".to_owned(), + actual, + }, + ); + None + } + } + }); + let inferred = elems + .iter() + .enumerate() + .map(|(index, elem)| { + self.infer_pat_expected( + body, + *elem, + expected_elems + .as_ref() + .and_then(|expected| expected.get(index).cloned()), + ) + }) + .collect::>(); + let ty = if self.pat_is_poisoned(body, pat) { + InferTy::Error + } else { + product_infer_ty(inferred) + }; + if let Some(expected) = expected { + self.unify_pat(body, pat, expected, ty.clone()); + } + ty + } + + fn infer_ctor_pat( + &mut self, + body: FuncBody<'db>, + pat: Id>, + args: &[Id>], + expected: Option>, + ) -> InferTy<'db> { + let resolution = self + .pat_resolutions + .get(&(body, pat)) + .cloned() + .unwrap_or(hir_nameres::Resolution::Err); + match resolution { + hir_nameres::Resolution::Ctor { ty, index } => { + let ctor_ty = self.instantiate_adt_ctor(ty, index, ObligationSource::Scheme); + let ret = expected.unwrap_or_else(|| self.engine.fresh_var()); + self.apply_ctor_pat_scheme(body, pat, args, ctor_ty, ret) + } + hir_nameres::Resolution::Builtin(kind) => { + let ctor_ty = self.infer_resolution_for_pat_builtin(kind); + let ret = expected.unwrap_or_else(|| self.engine.fresh_var()); + self.apply_ctor_pat_scheme(body, pat, args, ctor_ty, ret) + } + hir_nameres::Resolution::DotCtorDeferred => { + let name = match &body.pats(self.db).get(pat).kind { + PatKind::Ctor { head, .. } => (*head.name().atom()).text(self.db), + PatKind::Var(name) => (*name.atom()).text(self.db), + _ => "", + }; + let Some(expected) = expected else { + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + self.shorthand_ctor_diag( + self.pat_label_span(body, pat), + name, + "cannot resolve without expected constructor type".to_owned(), + ); + return InferTy::Error; + }; + match self.ctor_for_expected(name, expected.clone()) { + DotCtorLookup::Match { ty, .. } => { + self.apply_ctor_pat_scheme(body, pat, args, ty, expected) + } + DotCtorLookup::NoExpected => { + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + self.shorthand_ctor_diag( + self.pat_label_span(body, pat), + name, + "cannot resolve without expected constructor type".to_owned(), + ); + InferTy::Error + } + DotCtorLookup::NoMatch => { + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + self.shorthand_ctor_diag( + self.pat_label_span(body, pat), + name, + "no matching constructor".to_owned(), + ); + InferTy::Error + } + DotCtorLookup::Ambiguous(candidates) => { + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + self.shorthand_ctor_diag( + self.pat_label_span(body, pat), + name, + format!("ambiguous candidates: {}", candidates.join(", ")), + ); + InferTy::Error + } + } + } + hir_nameres::Resolution::Err => InferTy::Error, + _ => { + let name = match &body.pats(self.db).get(pat).kind { + PatKind::Ctor { head, .. } => (*head.name().atom()).text(self.db).to_owned(), + PatKind::Var(name) => (*name.atom()).text(self.db).to_owned(), + _ => "".to_owned(), + }; + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::InvalidConstructorPattern { + span: self.pat_label_span(body, pat), + name, + }, + ); + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + InferTy::Error + } + } + } + + fn infer_resolution_for_pat_builtin(&mut self, kind: hir_nameres::BuiltinKind) -> InferTy<'db> { + if let Some(scheme) = builtin_scheme(self.db, kind) { + let instantiated = self.engine.instantiate_scheme(scheme); + self.accept_instantiated(instantiated) + } else { + self.engine.fresh_var() + } + } + + fn apply_ctor_pat_scheme( + &mut self, + body: FuncBody<'db>, + pat: Id>, + args: &[Id>], + ctor_ty: InferTy<'db>, + expected: InferTy<'db>, + ) -> InferTy<'db> { + match self.engine.resolve(ctor_ty.clone()) { + InferTy::Function { params, ret } => { + if params.len() != args.len() { + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::WrongArity { + span: self.pat_label_span(body, pat), + context: "constructor pattern".to_owned(), + expected: params.len(), + actual: args.len(), + callee: None, + }, + ); + for (index, arg) in args.iter().enumerate() { + self.infer_pat_expected(body, *arg, params.get(index).cloned()); + } + return InferTy::Error; + } + let expected_params = args + .iter() + .map(|_| self.engine.fresh_var()) + .collect::>(); + self.unify_pat( + body, + pat, + ctor_ty.clone(), + InferTy::Function { + params: expected_params.clone(), + ret: Box::new(expected.clone()), + }, + ); + self.unify_pat(body, pat, *ret, expected.clone()); + let expected_params = expected_params + .into_iter() + .map(|param| self.engine.resolve(param)) + .collect::>(); + let inferred_args = args + .iter() + .enumerate() + .map(|(index, arg)| { + self.infer_pat_expected(body, *arg, expected_params.get(index).cloned()) + }) + .collect::>(); + self.unify_pat( + body, + pat, + ctor_ty, + InferTy::Function { + params: inferred_args, + ret: Box::new(expected.clone()), + }, + ); + expected + } + concrete => { + if matches!(concrete, InferTy::Error) { + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + self.poison_pat(body, pat); + return InferTy::Error; + } + if args.is_empty() { + if !self.unify_pat(body, pat, concrete.clone(), expected.clone()) { + return InferTy::Error; + } + } else { + let callee = self.display_infer_ty(concrete.clone()); + self.emit_pat_error( + body, + pat, + TypeckDiagnostic::NonCallable { + span: self.pat_label_span(body, pat), + callee, + }, + ); + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + return InferTy::Error; + } + for arg in args { + self.infer_pat_expected(body, *arg, None); + } + expected + } + } + } +} diff --git a/crates/hir-ty/src/infer/schemes.rs b/crates/hir-ty/src/infer/schemes.rs new file mode 100644 index 00000000..2ef324f2 --- /dev/null +++ b/crates/hir-ty/src/infer/schemes.rs @@ -0,0 +1,787 @@ +use super::*; + +/// Fixpoint iterations after which recursive signature inference is declared +/// divergent. A self-referential signature (e.g. `function f(x) { return f; }`) +/// grows its inferred type every round and never converges; without a bound +/// Salsa panics with "too many cycle iterations" instead of diagnosing. +const FUNCTION_SCHEME_MAX_FIXPOINT_ITERATIONS: u32 = 32; + +/// Lowers the scheme for one function-like definition in `module`. +#[salsa::tracked(cycle_fn = function_scheme_cycle, cycle_initial = function_scheme_cycle_initial)] +pub fn function_scheme<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + let hir_module = module_hir(db, module)?; + let env = nameres::module_env_for_hir_module(db, module, hir_module); + let scope = env.item_scope.clone()?; + let item_resolutions = + hir_nameres::resolve_item_type_facts_with_imports(db, hir_module, &scope, &env); + let info = find_function_info(db, hir_module, def)?; + let body_map = body_resolution_for_function_with_imports(db, hir_module, &info, Some(&env)); + Some( + lower_normalized_function_with_inferred_signature( + db, + hir_module, + &item_resolutions, + info.function, + &info.type_vars, + body_map.as_ref(), + Some(module), + ) + .scheme, + ) +} + +fn function_scheme_cycle<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + _last_provisional_value: &Option>, + value: Option>, + module: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + if cycle.iteration() >= FUNCTION_SCHEME_MAX_FIXPOINT_ITERATIONS { + // Pin the syntactic scheme so the fixpoint terminates; body checking + // then reports an ordinary type error for the divergent signature + // instead of the whole compiler panicking. + return function_scheme_cycle_initial(db, cycle.id(), module, def); + } + value +} + +fn function_scheme_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + module: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + let hir_module = module_hir(db, module)?; + let item_resolutions = item_resolution_facts_for_module(db, module)?; + let info = find_function_info(db, hir_module, def)?; + Some( + lower_normalized_function_syntactic( + db, + hir_module, + &item_resolutions, + info.function, + &info.type_vars, + ) + .scheme, + ) +} + +/// Lowers the scheme for one contract field in `module`. +#[salsa::tracked] +pub fn field_scheme<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + field: hir_nameres::FieldId<'db>, +) -> Option> { + let hir_module = module_hir(db, module)?; + let item_resolutions = item_resolution_facts_for_module(db, module)?; + field_scheme_in_module(db, hir_module, &item_resolutions, field) +} + +/// Lowers the scheme for one ADT constructor in `module`. +#[salsa::tracked] +pub fn adt_ctor_scheme<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, +) -> Option> { + let hir_module = module_hir(db, module)?; + let item_resolutions = item_resolution_facts_for_module(db, module)?; + adt_ctor_scheme_in_module(db, hir_module, &item_resolutions, ty, index) +} + +/// Lowers the scheme for one type-class method in `module`. +#[salsa::tracked] +pub fn class_method_scheme<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + class: DefId<'db>, + name: String, +) -> Option> { + let hir_module = module_hir(db, module)?; + let item_resolutions = item_resolution_facts_for_module(db, module)?; + class_method_scheme_in_module(db, hir_module, &item_resolutions, class, &name) +} + +pub(super) fn function_scheme_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + function_scheme(db, module_for_def(db, entry, def)?, def) +} + +pub(super) fn field_scheme_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + field: hir_nameres::FieldId<'db>, +) -> Option> { + field_scheme(db, module_for_def(db, entry, field.contract)?, field) +} + +pub(super) fn adt_ctor_scheme_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, +) -> Option> { + adt_ctor_scheme(db, module_for_def(db, entry, ty)?, ty, index) +} + +pub(super) fn adt_field_index_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + ty: DefId<'db>, + name: &str, +) -> Option<(hir_nameres::CtorIndex, u32)> { + let module = module_hir(db, module_for_def(db, entry, ty)?)?; + adt_field_index_in_module(db, module, ty, name) +} + +pub(super) fn adt_field_index_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + name: &str, +) -> Option<(hir_nameres::CtorIndex, u32)> { + adt_field_index_in_module(db, module, ty, name) +} + +pub(super) fn class_method_scheme_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + class: DefId<'db>, + name: String, +) -> Option> { + class_method_scheme(db, module_for_def(db, entry, class)?, class, name) +} + +pub(super) fn adt_ctor_schemes_by_name_for_entry<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + ty: DefId<'db>, + name: String, +) -> Vec> { + let Some(module) = module_for_def(db, entry, ty) else { + return Vec::new(); + }; + adt_ctor_indices_by_name(db, module, ty, name) + .into_iter() + .filter_map(|(index, ctor_name)| { + adt_ctor_scheme(db, module, ty, index).map(|scheme| AdtCtorScheme { + ty, + index, + name: ctor_name, + scheme, + }) + }) + .collect() +} + +#[salsa::tracked] +pub(super) fn module_for_def<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + crate::support::module_for_def_via_graph(db, entry, def) +} + +#[salsa::tracked] +pub(super) fn module_hir<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> Option> { + let file = db.module_file(module)?; + let source = parse_file_to_hir(db, file).module(db); + Some(crate::prepare_module(db, source).module(db)) +} + +#[salsa::tracked] +pub(super) fn item_resolutions_for_module<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Option> { + let hir_module = module_hir(db, module)?; + let env = nameres::module_env_for_hir_module(db, module, hir_module); + let scope = env.item_scope.clone()?; + Some(hir_nameres::resolve_item_types_with_imports( + db, hir_module, &scope, &env, + )) +} + +#[salsa::tracked] +pub(super) fn item_resolution_facts_for_module<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Option> { + let hir_module = module_hir(db, module)?; + let env = nameres::module_env_for_hir_module(db, module, hir_module); + let scope = env.item_scope.clone()?; + Some(hir_nameres::resolve_item_type_facts_with_imports( + db, hir_module, &scope, &env, + )) +} + +#[salsa::tracked(cycle_fn = function_scheme_in_hir_module_cycle, cycle_initial = function_scheme_in_hir_module_cycle_initial)] +pub(super) fn function_scheme_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + function_scheme_in_module(db, module, &item_resolutions, def) +} + +fn function_scheme_in_hir_module_cycle<'db>( + db: &'db dyn Db, + cycle: &salsa::Cycle, + _last_provisional_value: &Option>, + value: Option>, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + if cycle.iteration() >= FUNCTION_SCHEME_MAX_FIXPOINT_ITERATIONS { + return function_scheme_in_hir_module_cycle_initial(db, cycle.id(), module, def); + } + value +} + +fn function_scheme_in_hir_module_cycle_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + let info = find_function_info(db, module, def)?; + Some( + lower_normalized_function_syntactic( + db, + module, + &item_resolutions, + info.function, + &info.type_vars, + ) + .scheme, + ) +} + +#[salsa::tracked] +pub(super) fn field_scheme_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + field: hir_nameres::FieldId<'db>, +) -> Option> { + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + field_scheme_in_module(db, module, &item_resolutions, field) +} + +#[salsa::tracked] +pub(super) fn adt_ctor_scheme_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, +) -> Option> { + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + adt_ctor_scheme_in_module(db, module, &item_resolutions, ty, index) +} + +#[salsa::tracked] +pub(super) fn class_method_scheme_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + class: DefId<'db>, + name: String, +) -> Option> { + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + class_method_scheme_in_module(db, module, &item_resolutions, class, &name) +} + +#[salsa::tracked] +pub(super) fn adt_ctor_schemes_by_name_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + name: String, +) -> Vec> { + adt_ctor_indices_by_name_in_hir_module(db, module, ty, name) + .into_iter() + .filter_map(|(index, ctor_name)| { + adt_ctor_scheme_in_hir_module(db, module, ty, index).map(|scheme| AdtCtorScheme { + ty, + index, + name: ctor_name, + scheme, + }) + }) + .collect() +} + +#[salsa::tracked] +fn adt_ctor_indices_by_name<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + ty: DefId<'db>, + name: String, +) -> Vec<(hir_nameres::CtorIndex, String)> { + let Some(hir_module) = module_hir(db, module) else { + return Vec::new(); + }; + adt_ctor_indices_by_name_in_module(db, hir_module, ty, &name) +} + +#[salsa::tracked] +fn adt_ctor_indices_by_name_in_hir_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + name: String, +) -> Vec<(hir_nameres::CtorIndex, String)> { + adt_ctor_indices_by_name_in_module(db, module, ty, &name) +} + +pub(super) fn builtin_ctor_kind_by_name(name: &str) -> Option { + let ctor = match name { + "true" => hir_nameres::BuiltinCtor::True, + "false" => hir_nameres::BuiltinCtor::False, + "()" => hir_nameres::BuiltinCtor::Unit, + "pair" => hir_nameres::BuiltinCtor::Pair, + "inl" => hir_nameres::BuiltinCtor::Inl, + "inr" => hir_nameres::BuiltinCtor::Inr, + _ => return None, + }; + Some(hir_nameres::BuiltinKind::Constructor(ctor)) +} + +pub(super) fn ctor_result_ty<'db>(ty: &InferTy<'db>) -> InferTy<'db> { + match ty { + InferTy::Function { ret, .. } => (**ret).clone(), + ty => ty.clone(), + } +} + +fn function_scheme_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + def: DefId<'db>, +) -> Option> { + let info = find_function_info(db, module, def)?; + let body_map = body_resolution_for_function_with_imports(db, module, &info, None); + Some( + lower_normalized_function_with_inferred_signature( + db, + module, + item_resolutions, + info.function, + &info.type_vars, + body_map.as_ref(), + None, + ) + .scheme, + ) +} + +/// Lowers a legacy-inferred function signature, replacing omitted parameter +/// types with the generalized type inferred from its body when that inference +/// is clean. An omitted return type is the unit type. Complete-signature +/// diagnostics are owned by +/// `TypeckDiagnosticCollector` through `SignatureRequirement`; current +/// reference-aligned diagnostics reject incomplete top-level and contract +/// function signatures before this fallback is user-visible. +pub fn lower_normalized_function_with_inferred_signature<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + function: FunctionDef<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], + body_map: Option<&hir_nameres::BodyResolutionMap<'db>>, + entry_module: Option>, +) -> LoweredFunction<'db> { + let lowered = + lower_normalized_function_syntactic(db, module, item_resolutions, function, type_vars); + if !uses_legacy_inferred_signature(db, function) { + return lowered; + } + let Some(body) = function.body(db) else { + return lowered; + }; + let Some(body_map) = body_map else { + return lowered; + }; + if !body_map.diagnostics.is_empty() { + return lowered; + } + // An omitted return on a complete signature is unit. This legacy recovery + // path is reached only for missing parameter types, however, and using + // unit as an expectation would add cascading return/call diagnostics on + // top of SC0220. Infer the body return solely to keep recovery stable. + let recovery_ret = function.sig(db).ret.map(|_| lowered.ret); + let pre_typeck_desugar = crate::pre_typeck_desugar_body_tree(db, body); + let mut ctx = BodyTyContext::new( + module, + body_map.clone(), + type_vars.to_vec(), + lowered.params.clone(), + recovery_ret, + ) + .with_param_names(param_names(db, function.sig(db).params.atom())) + .with_ret_display( + function + .sig(db) + .ret + .map(|ret| crate::display::display_type_ref_source(db, ret)), + ) + .with_pre_typeck_desugar(pre_typeck_desugar); + if let Some(entry_module) = entry_module { + ctx = ctx.with_entry_module(entry_module); + } + let result = infer_body(db, body, ctx); + if !result.diagnostics.is_empty() { + return lowered; + } + let inferred_ty = result.root_scheme.body(db).ty(db); + let TyKind::Function { params, ret } = inferred_ty.kind(db) else { + return lowered; + }; + let scheme = TyScheme::new( + db, + result.root_scheme.binder_count(db), + QualTy::new(db, lowered.scheme.body(db).preds(db).clone(), inferred_ty), + ); + LoweredFunction { + scheme, + params: params.clone(), + ret: *ret, + } +} + +fn lower_normalized_function_syntactic<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + function: FunctionDef<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> LoweredFunction<'db> { + let lowered = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(type_vars), + ) + .lower_function(function); + normalize_lowered_function(db, module, item_resolutions, lowered) +} + +fn normalize_lowered_function<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + mut lowered: LoweredFunction<'db>, +) -> LoweredFunction<'db> { + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + lowered.scheme = normalizer.normalize_scheme(lowered.scheme); + lowered.params = lowered + .params + .into_iter() + .map(|param| normalizer.normalize_ty(param)) + .collect(); + lowered.ret = normalizer.normalize_ty(lowered.ret); + lowered +} + +fn uses_legacy_inferred_signature<'db>(db: &'db dyn HirDb, function: FunctionDef<'db>) -> bool { + if !matches!(function.kind(db), FuncKind::Function) { + return false; + } + let sig = function.sig(db); + sig.params + .atom() + .iter() + .any(|param| matches!(param, FuncParam::Untyped { .. } | FuncParam::Error { .. })) +} + +pub(super) fn body_resolution_for_function_with_imports<'db>( + db: &'db dyn Db, + module: Module<'db>, + info: &FunctionLookup<'db>, + imports: Option<&dyn hir_nameres::ImportedNames<'db>>, +) -> Option> { + let body = info.function.body(db)?; + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract: info.enclosing_contract, + params: param_bindings(info.function.sig(db).params.atom()), + type_vars: info.type_vars.clone(), + }; + Some(match imports { + Some(imports) => hir_nameres::resolve_body_with_imports_and_policy( + db, + body, + &context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ), + None => hir_nameres::resolve_body(db, body, context), + }) +} + +fn field_scheme_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + field: hir_nameres::FieldId<'db>, +) -> Option> { + let info = find_field_info(db, module, field)?; + let lowered = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_field(&info.field); + Some(AliasNormalizer::new(db, module, item_resolutions).normalize_scheme(lowered.scheme)) +} + +fn adt_ctor_scheme_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + ty: DefId<'db>, + index: hir_nameres::CtorIndex, +) -> Option> { + let info = find_adt_info(db, module, ty)?; + let ctor = info.adt.ctors(db).get(index.as_usize())?; + let lowered = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_adt_ctor(info.adt, ctor); + Some(AliasNormalizer::new(db, module, item_resolutions).normalize_scheme(lowered.scheme)) +} + +fn adt_field_index_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + name: &str, +) -> Option<(hir_nameres::CtorIndex, u32)> { + let info = find_adt_info(db, module, ty)?; + info.adt + .ctors(db) + .iter() + .enumerate() + .find_map(|(constructor, ctor)| { + let field_names = ctor.field_names.as_ref()?; + debug_assert_eq!(field_names.len(), ctor.field_count); + let index = field_names + .iter() + .position(|field| hir_nameres::ident_text(db, field) == name)?; + Some(( + hir_nameres::CtorIndex::from_usize(constructor), + u32::try_from(index).expect("ADT field index exceeds u32::MAX"), + )) + }) +} + +fn class_method_scheme_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + class: DefId<'db>, + name: &str, +) -> Option> { + let info = find_class_info(db, module, class)?; + let method = info + .class + .methods(db) + .iter() + .find(|method| ident_text(db, &method.name) == name)?; + let type_vars = class_method_type_vars(db, info.class, method); + let scheme = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ) + .lower_class_method(info.class, method); + Some(AliasNormalizer::new(db, module, item_resolutions).normalize_scheme(scheme)) +} + +fn adt_ctor_indices_by_name_in_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + ty: DefId<'db>, + name: &str, +) -> Vec<(hir_nameres::CtorIndex, String)> { + let Some(info) = find_adt_info(db, module, ty) else { + return Vec::new(); + }; + info.adt + .ctors(db) + .iter() + .enumerate() + .filter_map(|(index, ctor)| { + let ctor_name = ident_text(db, &ctor.name); + (ctor_name == name).then_some((hir_nameres::CtorIndex::from_usize(index), ctor_name)) + }) + .collect() +} + +/// Returns type-checking diagnostics for every module reachable from `entry`. +#[salsa::tracked(returns(ref))] +pub fn reachable_typeck_diagnostics<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, +) -> Vec { + let mut diagnostics = Vec::new(); + for module in nameres::reachable_modules(db, entry) { + diagnostics.extend(module_typeck_diagnostics(db, module).iter().cloned()); + } + sort_dedup_query_diagnostics(db, &mut diagnostics); + diagnostics +} + +/// Returns type-checking diagnostics for one module. +#[salsa::tracked(returns(ref))] +pub fn module_typeck_diagnostics<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Vec { + if matches!(module.library(db), LibraryId::Std) { + return Vec::new(); + } + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + if !parse_diagnostics(db, file).is_empty() { + return Vec::new(); + } + let source_module = parse_file_to_hir(db, file).module(db); + let prepared = crate::prepare_module(db, source_module); + let hir_module = prepared.module(db); + let env = nameres::module_env_for_hir_module(db, module, hir_module); + let Some(item_scope) = env.item_scope.clone() else { + return Vec::new(); + }; + let module_resolution = + hir_nameres::resolve_module_with_imports(db, hir_module, item_scope, &env); + let generated_nameres_diagnostics = if hir_module == source_module { + Vec::new() + } else { + let source_env = nameres::module_env_for_hir_module(db, module, source_module); + let source_diagnostics = source_env + .item_scope + .clone() + .map_or_else(Vec::new, |scope| { + hir_nameres::resolve_module_with_imports(db, source_module, scope, &source_env) + .diagnostics + }); + module_resolution + .diagnostics + .iter() + .filter(|diagnostic| !source_diagnostics.contains(diagnostic)) + // The source-facing SC0229 diagnostic below owns collisions with + // compiler-generated dispatch name types. Do not also expose the + // effective-HIR duplicate as SC0108. + .filter(|diagnostic| { + !matches!( + diagnostic, + hir_nameres::NameresDiagnostic::DuplicateDeclaration { name, .. } + if name.starts_with("DispatchNameTy_") + ) + }) + .cloned() + .collect::>() + }; + let item_resolutions = module_resolution.item_resolutions.clone(); + let trait_env = crate::solver::trait_env_from_module_resolution_and_imports( + db, + hir_module, + &module_resolution, + &env.import_surface(), + ); + let instance_diagnostics = instance_soundness_diagnostics(db, module); + let suppress_body_after_instance_error = instance_diagnostics + .iter() + .any(|diagnostic| matches!(diagnostic, TypeckDiagnostic::OverlappingInstance { .. })); + let mut diagnostics = instance_diagnostics + .iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())) + .collect::>(); + // The normal module-resolution driver already publishes source HIR + // diagnostics. Publish only diagnostics newly introduced by the effective + // module so generated-name failures do not become late specialization + // errors and source diagnostics are not duplicated at the typeck layer. + diagnostics.extend( + generated_nameres_diagnostics + .into_iter() + .map(AnyDiagnostic::Nameres), + ); + diagnostics.extend( + item_type_constructor_arity_diagnostics(db, module, &item_resolutions) + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + diagnostics.extend( + mutual_data_diagnostics(db, hir_module, &item_resolutions) + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + diagnostics.extend( + dispatch_name_collision_diagnostics(db, source_module) + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + let alias_errors = type_alias_normalization_errors(db, hir_module, &item_resolutions); + let alias_expansion_limit = alias_errors + .iter() + .any(|error| matches!(error, AliasError::ExpansionLimit { .. })); + diagnostics.extend( + alias_errors + .into_iter() + .map(alias_error_to_diagnostic) + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + if alias_expansion_limit { + sort_dedup_query_diagnostics(db, &mut diagnostics); + return diagnostics; + } + diagnostics.extend( + module_contract_diagnostics(db, source_module) + .into_iter() + .map(AnyDiagnostic::Typeck), + ); + diagnostics.extend( + module_manual_generic_abi_diagnostics(db, source_module, trait_env) + .into_iter() + .map(AnyDiagnostic::Typeck), + ); + diagnostics.extend( + crate::solver::generic_derivation_diagnostics(db, hir_module, &item_resolutions, &env) + .into_iter() + .map(|diagnostic| AnyDiagnostic::Typeck(diagnostic.lower())), + ); + if suppress_body_after_instance_error { + sort_dedup_query_diagnostics(db, &mut diagnostics); + return diagnostics; + } + let mut collector = TypeckDiagnosticCollector { + db, + module, + hir_module, + env, + item_resolutions, + trait_env, + diagnostics, + }; + for item in hir_module.items(db) { + collector.item(*item, None, &[]); + } + sort_dedup_query_diagnostics(db, &mut collector.diagnostics); + collector.diagnostics +} diff --git a/crates/hir-ty/src/infer/stmt.rs b/crates/hir-ty/src/infer/stmt.rs new file mode 100644 index 00000000..c9164c59 --- /dev/null +++ b/crates/hir-ty/src/infer/stmt.rs @@ -0,0 +1,301 @@ +use super::*; + +impl<'db> InferCtx<'db> { + pub(super) fn infer_body(&mut self, body: FuncBody<'db>) -> InferTy<'db> { + let top_level_stmts = body.top_level_stmts(self.db); + let ty = self.infer_stmt_sequence(body, top_level_stmts); + if let Some(expected) = self.return_stack.last().cloned() { + if let Some(last_stmt) = top_level_stmts.last().copied() { + if !self.is_return_stmt(body, last_stmt) { + self.unify_stmt(body, last_stmt, expected, ty.clone()); + } + } else { + self.unify_body(body, expected, ty.clone()); + } + } + ty + } + + fn infer_stmt_sequence( + &mut self, + body: FuncBody<'db>, + stmts: &[Id>], + ) -> InferTy<'db> { + if stmts.is_empty() { + return self.unit(); + } + let unit = self.unit(); + let mut result = unit.clone(); + for (index, stmt) in stmts.iter().enumerate() { + if index + 1 != stmts.len() && self.is_return_stmt(body, *stmt) { + self.diagnostics.push(TypeckDiagnostic::NonFinalReturn { + span: self.stmt_label_span(body, *stmt), + }); + } + result = self.infer_stmt(body, *stmt); + } + result + } + + fn is_return_stmt(&self, body: FuncBody<'db>, stmt_id: Id>) -> bool { + matches!(&body.stmts(self.db).get(stmt_id).kind, StmtKind::Return(_)) + } + + pub(super) fn lower_type_ref(&mut self, ty: TypeRef<'db>) -> InferTy<'db> { + let lowered = self.lowerer.lower_type(ty); + self.diagnostics.extend( + self.lowerer + .take_diagnostics() + .into_iter() + .map(lowering_diagnostic_to_typeck), + ); + self.engine.from_ty(lowered) + } + + fn infer_stmt(&mut self, body: FuncBody<'db>, stmt_id: Id>) -> InferTy<'db> { + let stmt = body.stmts(self.db).get(stmt_id); + if let StmtKind::Assign { lhs, .. } = &stmt.kind + && let Some(conversion) = self.conversion_in_assignment_target(body, *lhs) + { + self.diagnostics + .push(TypeckDiagnostic::ConversionAssignmentTarget { + span: self.expr_label_span(body, conversion), + }); + self.poison_expr(body, conversion); + } + match &stmt.kind { + StmtKind::Let { + comptime, + name, + ty, + init, + } => { + let declared_comptime = comptime.is_some() + || type_ref_is_comptime(self.db, ty.as_ref()) + || ty + .as_ref() + .is_some_and(|ty| type_ref_is_integer(self.db, *ty)); + let local_ty = ty + .map(|ty| self.lower_type_ref(ty)) + .unwrap_or_else(|| self.engine.fresh_var()); + let local_ty = self.maybe_comptime(*comptime, local_ty); + let mut local_ty = local_ty; + if let Some(init) = init { + let init_ty = if ty.is_none() + && comptime.is_none() + && matches!(body.exprs(self.db).get(*init).kind, ExprKind::Lambda { .. }) + { + self.infer_expr(body, *init) + } else { + self.infer_expr_expected(body, *init, Some(local_ty.clone())) + }; + self.unify_expr(body, *init, local_ty.clone(), init_ty); + if self.expr_is_poisoned(body, *init) { + local_ty = InferTy::Error; + } + self.pending_comptime_lets.push(PendingComptimeLet { + body, + stmt: stmt_id, + expr: *init, + name: (*name.atom()).text(self.db).to_owned(), + declared: declared_comptime, + ty: local_ty.clone(), + }); + } + self.let_tys.insert((body, stmt_id), local_ty); + let name = (*name.atom()).text(self.db).to_owned(); + let ty = self.let_ty(body, stmt_id); + self.add_sail_local(name, ty); + self.unit() + } + StmtKind::Return(expr) => { + if let Some(expected) = self.return_stack.last().cloned() { + if infer_ty_has_comptime_wrapper(&self.engine.resolve(expected.clone())) + && let Some(expr) = expr + { + self.comptime_obligations.push(ComptimeObligation { + body, + expr: *expr, + kind: ComptimeObligationKind::Return { + context: self.body_context(body), + }, + }); + } + if let Some(expr) = expr { + if let Some(display) = self.return_display_stack.last().cloned().flatten() { + self.expected_expr_displays.insert((body, *expr), display); + } + let actual = self.infer_expr_expected(body, *expr, Some(expected.clone())); + self.unify_expr(body, *expr, expected, actual.clone()); + actual + } else { + let actual = self.unit(); + self.unify_stmt(body, stmt_id, expected, actual.clone()); + actual + } + } else { + expr.map(|expr| self.infer_expr(body, expr)) + .unwrap_or_else(|| self.unit()) + } + } + StmtKind::Expr(expr) => { + self.infer_expr(body, *expr); + self.unit() + } + StmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs, + } => { + if !self.infer_storage_assign(body, *lhs, *rhs) { + let lhs_ty = self.infer_expr(body, *lhs); + let rhs_ty = self.infer_expr_expected(body, *rhs, Some(lhs_ty.clone())); + self.unify_expr(body, *rhs, lhs_ty, rhs_ty); + } + self.unit() + } + StmtKind::Assign { + op: AssignOp::Add | AssignOp::Sub, + lhs, + rhs, + } if self.is_storage_index_expr(body, *lhs) => { + let lhs_ty = self.infer_expr(body, *lhs); + // The reference elaborates `m[k] += v` to `m[k] = m[k] + v` + // through Add.add, but our indexed compound assignment still + // lowers to raw word add/sub. Gate the element type to word or + // the std word-backed numeric newtypes, where the instance + // semantics coincide with the raw lowering; anything else + // (bool, address, custom instances) is a type error here. + if !self.is_storage_index_word_numeric(lhs_ty.clone()) { + let word = self.word(); + self.unify_expr(body, *lhs, lhs_ty.clone(), word); + } + let rhs_ty = self.infer_expr_expected(body, *rhs, Some(lhs_ty.clone())); + self.unify_expr(body, *rhs, lhs_ty, rhs_ty); + self.unit() + } + StmtKind::Assign { + op: + AssignOp::Add + | AssignOp::Sub + | AssignOp::BitXor + | AssignOp::BitAnd + | AssignOp::BitOr + | AssignOp::Mod, + lhs, + rhs, + } => { + let lhs_ty = self.infer_expr(body, *lhs); + let rhs_ty = self.infer_expr(body, *rhs); + let word = self.word(); + self.unify_expr(body, *lhs, lhs_ty, word.clone()); + self.unify_expr(body, *rhs, rhs_ty, word); + self.unit() + } + StmtKind::Match { scrutinees, arms } => { + let scrutinee_tys = scrutinees + .iter() + .map(|scrutinee| self.infer_expr(body, *scrutinee)) + .collect::>(); + self.ensure_visible_pattern_coverage(body, scrutinees, &scrutinee_tys, arms); + let result_ty = self.engine.fresh_var(); + for arm in arms { + let arm_ty = self.infer_match_arm(body, arm, &scrutinee_tys); + self.unify_span(arm.span(self.db), result_ty.clone(), arm_ty); + } + self.ensure_match_coverage(body, scrutinees, &scrutinee_tys, arms); + result_ty + } + StmtKind::For { + init, + cond, + post, + body: for_body, + } => { + self.infer_stmt_sequence(body, init); + let cond_ty = self.infer_expr(body, *cond); + let bool_ty = self.bool(); + self.unify_expr(body, *cond, cond_ty, bool_ty); + self.infer_stmt_sequence(body, post); + self.infer_stmt_sequence(body, for_body); + self.unit() + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + let input = + self.if_stmt_match_input(body, stmt_id, *cond, then_body, else_body.as_deref()); + let cond_ty = self.infer_expr(body, input.cond); + let bool_ty = self.bool(); + self.unify_expr(body, input.cond, cond_ty, bool_ty); + let then_ty = self.infer_stmt_sequence(body, &input.then_body); + let else_ty = input + .else_body + .as_ref() + .map(|else_body| self.infer_stmt_sequence(body, else_body)) + .unwrap_or_else(|| then_ty.clone()); + self.unify_stmt(body, stmt_id, then_ty.clone(), else_ty); + then_ty + } + StmtKind::Block { body: block } => { + self.push_sail_scope(); + let ty = self.infer_stmt_sequence(body, block); + self.pop_sail_scope(); + ty + } + StmtKind::Assembly { body: yul_body } => { + let (new_binds, ty) = self.infer_yul_block(yul_body); + let word = self.word(); + for name in new_binds { + self.add_sail_local(name, word.clone()); + } + ty + } + StmtKind::Break | StmtKind::Continue => self.unit(), + StmtKind::Error => InferTy::Error, + } + } + + fn conversion_in_assignment_target( + &self, + body: FuncBody<'db>, + expr: Id>, + ) -> Option>> { + match &body.exprs(self.db).get(expr).kind { + ExprKind::Conversion { .. } => Some(expr), + ExprKind::Index { base, .. } + | ExprKind::Field { base, .. } + | ExprKind::TypeAscription { expr: base, .. } => { + self.conversion_in_assignment_target(body, *base) + } + _ => None, + } + } + + fn infer_match_arm( + &mut self, + body: FuncBody<'db>, + arm: &MatchArm<'db>, + scrutinees: &[InferTy<'db>], + ) -> InferTy<'db> { + if arm.pats.len() != scrutinees.len() { + self.diagnostics.push(TypeckDiagnostic::WrongArity { + span: self.label_span(arm.span(self.db)), + context: "match arm".to_owned(), + expected: scrutinees.len(), + actual: arm.pats.len(), + callee: None, + }); + } + self.push_sail_scope(); + for (pat, scrutinee) in arm.pats.iter().zip(scrutinees.iter()) { + let pat_ty = self.infer_pat_expected(body, *pat, Some(scrutinee.clone())); + self.unify_pat(body, *pat, scrutinee.clone(), pat_ty); + } + let ty = self.infer_stmt_sequence(body, &arm.body); + self.pop_sail_scope(); + ty + } +} diff --git a/crates/hir-ty/src/infer/storage.rs b/crates/hir-ty/src/infer/storage.rs new file mode 100644 index 00000000..4dcb95f2 --- /dev/null +++ b/crates/hir-ty/src/infer/storage.rs @@ -0,0 +1,497 @@ +use super::*; + +impl<'db> InferCtx<'db> { + pub(super) fn infer_storage_index_read( + &mut self, + body: FuncBody<'db>, + expr: Id>, + base: Id>, + index: Id>, + ) -> Option> { + if !self.is_storage_index_expr(body, base) { + return None; + } + let base_ty = self.infer_storage_ref_expr(body, base, true)?; + let (index_ty, value_ty) = self.storage_mapping_args(base_ty)?; + let actual_index_ty = self.infer_expr_expected(body, index, Some(index_ty.clone())); + self.unify_expr(body, index, index_ty, actual_index_ty); + Some(self.storage_load_ty(body, expr, value_ty)) + } + + pub(super) fn infer_storage_assign( + &mut self, + body: FuncBody<'db>, + lhs: Id>, + rhs: Id>, + ) -> bool { + let Some(lhs_ty) = self.infer_storage_ref_expr(body, lhs, false) else { + return false; + }; + let expected_rhs = self + .loaded_ty_for_storage_ty(lhs_ty.clone()) + .unwrap_or_else(|| self.engine.fresh_var()); + let rhs_ty = self.infer_expr_expected(body, rhs, Some(expected_rhs.clone())); + self.unify_expr(body, rhs, expected_rhs, rhs_ty.clone()); + self.push_can_store_obligation(body, lhs, lhs_ty, rhs_ty.clone(), ObligationSource::Scheme); + self.expr_tys.push((body, lhs, rhs_ty)); + true + } + + fn infer_storage_ref_expr( + &mut self, + body: FuncBody<'db>, + expr: Id>, + record_current: bool, + ) -> Option> { + let kind = body.exprs(self.db).get(expr).kind.clone(); + let ty = match kind { + ExprKind::Index { base, index } => { + let base_ty = self.infer_storage_ref_expr(body, base, true)?; + let (index_ty, value_ty) = self.storage_mapping_args(base_ty)?; + let actual_index_ty = self.infer_expr_expected(body, index, Some(index_ty.clone())); + self.unify_expr(body, index, index_ty, actual_index_ty); + Some(value_ty) + } + ExprKind::TypeAscription { expr: inner, .. } => { + self.infer_storage_ref_expr(body, inner, true) + } + _ => match self.expr_resolutions.get(&(body, expr)).cloned() { + Some(hir_nameres::Resolution::Field(field)) => { + Some(self.instantiate_field_ref(field, ObligationSource::Scheme)) + } + _ => None, + }, + }?; + if record_current { + self.expr_tys.push((body, expr, ty.clone())); + } + Some(ty) + } + + pub(super) fn is_storage_index_expr(&self, body: FuncBody<'db>, expr: Id>) -> bool { + if matches!( + self.expr_resolutions.get(&(body, expr)), + Some(hir_nameres::Resolution::Field(_)) + ) { + return true; + } + match &body.exprs(self.db).get(expr).kind { + ExprKind::Index { base, .. } => self.is_storage_index_expr(body, *base), + ExprKind::TypeAscription { expr, .. } => self.is_storage_index_expr(body, *expr), + _ => false, + } + } + + pub(super) fn reject_storage_field_projection( + &mut self, + body: FuncBody<'db>, + expr: Id>, + base: Id>, + ) -> bool { + if !self.is_storage_index_expr(body, base) { + return false; + } + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsupportedStorageFieldProjection { + span: self.field_label_span(body, expr), + field: self.field_name(body, expr), + }, + ); + true + } + + fn storage_mapping_args(&mut self, ty: InferTy<'db>) -> Option<(InferTy<'db>, InferTy<'db>)> { + let storage_ctor = self.storage_type_ctor(); + let ty = self.normalize_aliases(ty); + let mut resolved = self.engine.resolve(ty); + if let Some(storage_ctor) = storage_ctor + && let InferTy::Named { ctor, args } = &resolved + && *ctor == storage_ctor + && args.len() == 1 + { + let inner = self.normalize_aliases(args[0].clone()); + resolved = self.engine.resolve(inner); + } + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } = resolved + else { + return None; + }; + if def.name(self.db).as_deref() != Some("mapping") || args.len() != 2 { + return None; + } + let value = if let Some(storage_ctor) = storage_ctor { + InferTy::Named { + ctor: storage_ctor, + args: vec![args[1].clone()], + } + } else { + args[1].clone() + }; + Some((args[0].clone(), value)) + } + + fn storage_type_ctor(&self) -> Option> { + self.lookup_type_resolution("storage") + .and_then(type_ctor_from_resolution) + } + + fn memory_type_ctor(&self) -> Option> { + self.lookup_type_resolution("memory") + .and_then(type_ctor_from_resolution) + } + + fn lookup_class_id(&self, name: &str) -> Option> { + self.lookup_type_resolution(name) + .and_then(class_id_from_resolution) + } + + fn lookup_type_resolution(&self, name: &str) -> Option> { + if let Some(module_id) = self + .entry_module + .or_else(|| module_id_for_hir_module(self.db, self.module)) + { + let env = nameres::module_import_surface(self.db, module_id); + let local = env + .item_scope + .as_ref() + .and_then(|scope| scope.type_resolution(name)); + return local.or_else(|| env.types.get(name).cloned()); + } + + hir_nameres::item_scope_facts(self.db, self.module).type_resolution(name) + } + + fn instantiate_field_ref( + &mut self, + field: hir_nameres::FieldId<'db>, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + let ty = self.instantiate_field(field, source); + if let Some(storage_ctor) = self.storage_type_ctor() { + InferTy::Named { + ctor: storage_ctor, + args: vec![ty], + } + } else { + ty + } + } + + pub(super) fn instantiate_field_read( + &mut self, + body: FuncBody<'db>, + expr: Id>, + field: hir_nameres::FieldId<'db>, + source: ObligationSource<'db>, + ) -> InferTy<'db> { + let field_ref = self.instantiate_field_ref(field, source); + self.storage_load_ty(body, expr, field_ref) + } + + fn storage_load_ty( + &mut self, + body: FuncBody<'db>, + expr: Id>, + storage_ty: InferTy<'db>, + ) -> InferTy<'db> { + if self.storage_type_ctor().is_none() { + return storage_ty; + } + let loaded = self + .loaded_ty_for_storage_ty(storage_ty.clone()) + .unwrap_or_else(|| self.engine.fresh_var()); + self.push_can_store_obligation( + body, + expr, + storage_ty, + loaded.clone(), + ObligationSource::Scheme, + ); + loaded + } + + fn loaded_ty_for_storage_ty(&mut self, ty: InferTy<'db>) -> Option> { + let Some(storage_ctor) = self.storage_type_ctor() else { + return Some(ty); + }; + let ty = self.normalize_aliases(ty); + let InferTy::Named { ctor, args } = self.engine.resolve(ty.clone()) else { + return None; + }; + if ctor != storage_ctor || args.len() != 1 { + return None; + } + let inner = self.normalize_aliases(args[0].clone()); + let inner = self.engine.resolve(inner); + if self.is_mapping_adt_ty(inner.clone()) { + return Some(InferTy::Named { + ctor: storage_ctor, + args: vec![inner], + }); + } + if self.is_memory_backed_storage_adt(inner.clone()) { + let memory_ctor = self.memory_type_ctor()?; + return Some(InferTy::Named { + ctor: memory_ctor, + args: vec![inner], + }); + } + Some(inner) + } + + fn is_mapping_adt_ty(&mut self, ty: InferTy<'db>) -> bool { + self.is_named_adt_ty(ty, "mapping", Some(2)) + } + + fn is_memory_backed_storage_adt(&mut self, ty: InferTy<'db>) -> bool { + self.is_named_adt_ty(ty.clone(), "string", Some(0)) + || self.is_named_adt_ty(ty, "bytes", Some(0)) + } + + fn is_named_adt_ty(&mut self, ty: InferTy<'db>, name: &str, arity: Option) -> bool { + let ty = self.normalize_aliases(ty); + let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } = self.engine.resolve(ty) + else { + return false; + }; + def.name(self.db).as_deref() == Some(name) && arity.is_none_or(|arity| args.len() == arity) + } + + fn push_can_store_obligation( + &mut self, + body: FuncBody<'db>, + expr: Id>, + storage_ty: InferTy<'db>, + loaded_ty: InferTy<'db>, + source: ObligationSource<'db>, + ) { + let resolved_loaded = self.engine.resolve(loaded_ty.clone()); + let structurally_fixed = self.infer_ty_contains_fixed_array(resolved_loaded.clone()); + let grounded_loaded = self.engine.ground_ty(resolved_loaded); + if structurally_fixed + || self.ty_contains_fixed_array_in_layout(grounded_loaded, &mut FxHashSet::default()) + { + let ty = self.display_infer_ty(loaded_ty); + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsupportedFixedArrayStorage { + span: self.expr_label_span(body, expr), + ty, + }, + ); + return; + } + if let InferTy::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::ValueType, + }), + args, + } = self.engine.resolve(loaded_ty.clone()) + && args.is_empty() + { + let item_resolutions = self.item_resolutions_for_aliases(); + if let Ok(underlying) = + value_type_underlying_in_context(self.db, self.module, &item_resolutions, def) + { + if !value_type_underlying_has_word_storage_representation(self.db, underlying) { + self.emit_expr_error( + body, + expr, + TypeckDiagnostic::UnsupportedValueTypeStorage { + span: self.expr_label_span(body, expr), + ty: def + .name(self.db) + .unwrap_or_else(|| "".to_owned()), + }, + ); + return; + } + // A valid UDVT has exactly the storage representation of its + // word-like underlying elementary value type. Its nominal + // identity is restored on load and checked on assignment + // before this point. + return; + } + } + let Some(class) = self.lookup_class_id("CanStore") else { + return; + }; + self.pending.push(PendingObligation { + class, + main: storage_ty, + args: vec![loaded_ty], + source, + }); + } + + fn infer_ty_contains_fixed_array(&mut self, ty: InferTy<'db>) -> bool { + let ty = self.normalize_aliases(ty); + match self.engine.resolve(ty) { + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => true, + InferTy::Named { args, .. } | InferTy::Tuple(args) => args + .into_iter() + .any(|arg| self.infer_ty_contains_fixed_array(arg)), + InferTy::Function { params, ret } => { + params + .into_iter() + .any(|param| self.infer_ty_contains_fixed_array(param)) + || self.infer_ty_contains_fixed_array(*ret) + } + InferTy::Comptime(inner) => self.infer_ty_contains_fixed_array(*inner), + InferTy::Error | InferTy::Unknown | InferTy::Var(_) | InferTy::BoundVar(_) => false, + } + } + + fn ty_contains_fixed_array_in_layout( + &self, + ty: Ty<'db>, + visiting: &mut FxHashSet>, + ) -> bool { + match ty.kind(self.db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => true, + TyKind::Named { ctor, args } => { + if args + .iter() + .any(|arg| self.ty_contains_fixed_array_in_layout(*arg, visiting)) + { + return true; + } + let TyCtor::User(user) = ctor else { + return false; + }; + if user.kind != UserTyCtorKind::Adt || !visiting.insert(user.def) { + return false; + } + let contains = self + .adt_layout_field_types(user.def, args) + .into_iter() + .any(|field| self.ty_contains_fixed_array_in_layout(field, visiting)); + visiting.remove(&user.def); + contains + } + TyKind::Function { params, ret } => { + params + .iter() + .any(|param| self.ty_contains_fixed_array_in_layout(*param, visiting)) + || self.ty_contains_fixed_array_in_layout(*ret, visiting) + } + TyKind::Tuple(elems) => elems + .iter() + .any(|elem| self.ty_contains_fixed_array_in_layout(*elem, visiting)), + TyKind::Comptime(inner) => self.ty_contains_fixed_array_in_layout(*inner, visiting), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + } + } + + fn adt_layout_field_types(&self, def: DefId<'db>, args: &[Ty<'db>]) -> Vec> { + if let Some(info) = find_adt_info(self.db, self.module, def) { + let item_resolutions = self.item_resolutions_for_aliases(); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ); + let mut fields = Vec::new(); + for ctor in info.adt.ctors(self.db) { + let lowered = lowerer.lower_adt_ctor(info.adt, ctor); + let mut normalizer = AliasNormalizer::new(self.db, self.module, &item_resolutions); + fields.extend(lowered.params.into_iter().map(|field| { + let field = normalizer.normalize_ty(field); + substitute_storage_bound_ty(self.db, field, args) + })); + } + return fields; + } + + let Some(module) = self + .entry_module + .and_then(|entry| module_for_def(self.db, entry, def)) + else { + return Vec::new(); + }; + let Some(hir_module) = module_hir(self.db, module) else { + return Vec::new(); + }; + let Some(info) = find_adt_info(self.db, hir_module, def) else { + return Vec::new(); + }; + info.adt + .ctors(self.db) + .iter() + .enumerate() + .filter_map(|(index, _)| { + adt_ctor_scheme( + self.db, + module, + def, + hir_nameres::CtorIndex::from_usize(index), + ) + }) + .flat_map( + |scheme| match scheme.body(self.db).ty(self.db).kind(self.db) { + TyKind::Function { params, .. } => params + .iter() + .map(|field| substitute_storage_bound_ty(self.db, *field, args)) + .collect(), + _ => Vec::new(), + }, + ) + .collect() + } +} + +fn substitute_storage_bound_ty<'db>(db: &'db dyn Db, ty: Ty<'db>, args: &[Ty<'db>]) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => args.get(var.index as usize).copied().unwrap_or(ty), + TyKind::Named { ctor, args: inner } => Ty::named( + db, + *ctor, + inner + .iter() + .map(|arg| substitute_storage_bound_ty(db, *arg, args)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| substitute_storage_bound_ty(db, *param, args)) + .collect(), + substitute_storage_bound_ty(db, *ret, args), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| substitute_storage_bound_ty(db, *elem, args)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, substitute_storage_bound_ty(db, *inner, args)), + TyKind::Error | TyKind::Unknown => ty, + } +} diff --git a/crates/hir-ty/src/infer/table.rs b/crates/hir-ty/src/infer/table.rs new file mode 100644 index 00000000..7f98a134 --- /dev/null +++ b/crates/hir-ty/src/infer/table.rs @@ -0,0 +1,557 @@ +use super::*; +use crate::display::display_ty_source; + +/// Ephemeral inference variable identifier. +/// +/// `TyVid` values are allocated inside one [`InferTable`] and must not cross a +/// Salsa query boundary. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct TyVid<'db> { + index: u32, + _marker: PhantomData<&'db ()>, +} + +impl<'db> Clone for TyVid<'db> { + fn clone(&self) -> Self { + *self + } +} + +impl<'db> Copy for TyVid<'db> {} + +impl<'db> TyVid<'db> { + /// Returns the variable's table-local index. + pub const fn index(self) -> u32 { + self.index + } +} + +impl<'db> UnifyKey for TyVid<'db> { + type Value = VarValue<'db>; + + fn index(&self) -> u32 { + self.index + } + + fn from_index(index: u32) -> Self { + Self { + index, + _marker: PhantomData, + } + } + + fn tag() -> &'static str { + "TyVid" + } +} + +/// Value stored for each ena type variable. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VarValue<'db> { + /// The variable has been solved to an inference type. + Known(InferTy<'db>), + /// The variable is not solved yet. + Unknown, +} + +impl<'db> UnifyValue for VarValue<'db> { + type Error = NoError; + + fn unify_values(value1: &Self, value2: &Self) -> Result { + Ok(match (value1, value2) { + (Self::Known(value), _) | (_, Self::Known(value)) => Self::Known(value.clone()), + (Self::Unknown, Self::Unknown) => Self::Unknown, + }) + } +} + +/// Ephemeral inference type. +/// +/// This mirrors the ground `Ty` shape but may contain ena variables. It is used +/// only while an inference query is executing. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum InferTy<'db> { + /// Error sentinel. + Error, + /// Unknown wildcard. + Unknown, + /// Ephemeral inference variable. + Var(TyVid<'db>), + /// De Bruijn-bound rigid variable. + BoundVar(u32), + /// Type constructor application. + Named { + /// Resolved constructor. + ctor: TyCtor<'db>, + /// Type arguments. + args: Vec>, + }, + /// Function type. + Function { + /// Parameter types. + params: Vec>, + /// Return type. + ret: Box>, + }, + /// Tuple type, including unit. + Tuple(Vec>), + /// `comptime` type wrapper. + Comptime(Box>), +} + +impl<'db> AliasType<'db> for InferTy<'db> { + fn alias_kind(&self, _db: &'db dyn Db) -> AliasTypeKind<'db, Self> { + match self { + InferTy::Error => AliasTypeKind::Error, + InferTy::Unknown => AliasTypeKind::Unknown, + InferTy::Var(var) => AliasTypeKind::BoundVar(var.index()), + InferTy::BoundVar(index) => AliasTypeKind::BoundVar(*index), + InferTy::Named { ctor, args } => AliasTypeKind::Named { + ctor: *ctor, + args: args.clone(), + }, + InferTy::Function { params, ret } => AliasTypeKind::Function { + params: params.clone(), + ret: (**ret).clone(), + }, + InferTy::Tuple(elems) => AliasTypeKind::Tuple(elems.clone()), + InferTy::Comptime(inner) => AliasTypeKind::Comptime((**inner).clone()), + } + } + + fn alias_error(_db: &'db dyn Db) -> Self { + InferTy::Error + } + + fn alias_bound(_db: &'db dyn Db, index: u32) -> Self { + InferTy::BoundVar(index) + } + + fn alias_named(_db: &'db dyn Db, ctor: TyCtor<'db>, args: Vec) -> Self { + InferTy::Named { ctor, args } + } + + fn alias_function(_db: &'db dyn Db, params: Vec, ret: Self) -> Self { + InferTy::Function { + params, + ret: Box::new(ret), + } + } + + fn alias_tuple(_db: &'db dyn Db, elems: Vec) -> Self { + InferTy::Tuple(elems) + } + + fn alias_comptime(_db: &'db dyn Db, inner: Self) -> Self { + InferTy::Comptime(Box::new(inner)) + } +} + +/// Unification failure from the ephemeral unifier. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnifyError<'db> { + /// Two concrete type shapes could not be unified. + Mismatch { + /// Expected or left-hand type. + expected: InferTy<'db>, + /// Actual or right-hand type. + actual: InferTy<'db>, + }, + /// Binding a variable would create an infinite type. + Occurs { + /// Variable being bound. + var: TyVid<'db>, + /// Type that already contains the variable. + ty: InferTy<'db>, + }, +} + +/// Result of instantiating a polymorphic scheme. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Instantiated<'db> { + /// Instantiated body type. + pub ty: InferTy<'db>, + pub(super) obligations: Vec>, + pub(super) equality_errors: Vec>, +} + +/// Ephemeral ena-backed unification table. +pub struct InferTable<'db> { + db: &'db dyn Db, + pub(super) table: InPlaceUnificationTable>, +} + +impl<'db> InferTable<'db> { + /// Creates an empty ephemeral unification table. + pub fn new(db: &'db dyn Db) -> Self { + Self { + db, + table: InPlaceUnificationTable::new(), + } + } + + /// Allocates a fresh inference variable. + pub fn fresh_vid(&mut self) -> TyVid<'db> { + self.table.new_key(VarValue::Unknown) + } + + /// Allocates a fresh inference variable as an `InferTy`. + pub fn fresh_var(&mut self) -> InferTy<'db> { + InferTy::Var(self.fresh_vid()) + } + + /// Converts a ground type into an inference type. + pub fn from_ty(&mut self, ty: Ty<'db>) -> InferTy<'db> { + self.infer_from_ty(ty) + } + + /// Instantiates a scheme by replacing de Bruijn binders with fresh vars. + pub fn instantiate_scheme(&mut self, scheme: TyScheme<'db>) -> Instantiated<'db> { + self.instantiate_scheme_with_source(scheme, ObligationSource::Scheme) + } + + /// Instantiates a scheme and assigns one source to all instantiated + /// predicates. + pub fn instantiate_scheme_with_source( + &mut self, + scheme: TyScheme<'db>, + source: ObligationSource<'db>, + ) -> Instantiated<'db> { + let vars = (0..scheme.binder_count(self.db)) + .map(|_| self.fresh_var()) + .collect::>(); + let body = scheme.body(self.db); + let ty = self.instantiate_ty(body.ty(self.db), &vars); + let mut obligations = Vec::new(); + let mut equality_errors = Vec::new(); + for pred in body.preds(self.db) { + match self.instantiate_pred(*pred, &vars, source.clone()) { + InstantiatedPred::Obligation(obligation) => obligations.push(obligation), + InstantiatedPred::EqualityError(error) => equality_errors.push(error), + InstantiatedPred::None => {} + } + } + Instantiated { + ty, + obligations, + equality_errors, + } + } + + /// Attempts to unify two inference types transactionally. + /// + /// On failure, all table changes made by the attempt are rolled back. + pub fn unify( + &mut self, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> Result<(), UnifyError<'db>> { + let snapshot = self.table.snapshot(); + match self.unify_inner(expected, actual) { + Ok(()) => { + self.table.commit(snapshot); + Ok(()) + } + Err(err) => { + self.table.rollback_to(snapshot); + Err(err) + } + } + } + + /// Returns whether two types can unify, rolling back either way. + pub fn can_unify(&mut self, expected: InferTy<'db>, actual: InferTy<'db>) -> bool { + let snapshot = self.table.snapshot(); + let ok = self.unify_inner(expected, actual).is_ok(); + self.table.rollback_to(snapshot); + ok + } + + /// Resolves an inference type through current variable bindings. + pub fn resolve(&mut self, ty: InferTy<'db>) -> InferTy<'db> { + match ty { + InferTy::Var(var) => { + let root = self.table.find(var); + match self.table.probe_value(root) { + VarValue::Known(ty) => self.resolve(ty), + VarValue::Unknown => InferTy::Var(root), + } + } + InferTy::Named { ctor, args } => InferTy::Named { + ctor, + args: args.into_iter().map(|arg| self.resolve(arg)).collect(), + }, + InferTy::Function { params, ret } => InferTy::Function { + params: params + .into_iter() + .map(|param| self.resolve(param)) + .collect(), + ret: Box::new(self.resolve(*ret)), + }, + InferTy::Tuple(elems) => { + InferTy::Tuple(elems.into_iter().map(|elem| self.resolve(elem)).collect()) + } + InferTy::Comptime(inner) => InferTy::Comptime(Box::new(self.resolve(*inner))), + ty @ (InferTy::Error | InferTy::Unknown | InferTy::BoundVar(_)) => ty, + } + } + + /// Converts an inference type to a ground type, replacing unresolved vars + /// with `Ty::unknown`. + pub fn ground_ty(&mut self, ty: InferTy<'db>) -> Ty<'db> { + match self.resolve(ty) { + InferTy::Error => Ty::error(self.db), + InferTy::Unknown | InferTy::Var(_) => Ty::unknown(self.db), + InferTy::BoundVar(index) => Ty::bound(self.db, index), + InferTy::Named { ctor, args } => Ty::named( + self.db, + ctor, + args.into_iter().map(|arg| self.ground_ty(arg)).collect(), + ), + InferTy::Function { params, ret } => Ty::function( + self.db, + params + .into_iter() + .map(|param| self.ground_ty(param)) + .collect(), + self.ground_ty(*ret), + ), + InferTy::Tuple(elems) => Ty::tuple( + self.db, + elems.into_iter().map(|elem| self.ground_ty(elem)).collect(), + ), + InferTy::Comptime(inner) => Ty::comptime(self.db, self.ground_ty(*inner)), + } + } + + /// Returns a diagnostic snapshot for an inference type. + pub fn display(&mut self, ty: InferTy<'db>) -> String { + self.display_with_names(ty, &[]) + } + + pub(super) fn display_with_names(&mut self, ty: InferTy<'db>, names: &[String]) -> String { + let ty = self.ground_ty(ty); + display_ty_source(self.db, ty, names) + } + + fn infer_from_ty(&mut self, ty: Ty<'db>) -> InferTy<'db> { + match ty.kind(self.db) { + TyKind::Error => InferTy::Error, + TyKind::Unknown => self.fresh_var(), + TyKind::BoundVar(var) => InferTy::BoundVar(var.index), + TyKind::Named { ctor, args } => InferTy::Named { + ctor: *ctor, + args: args.iter().map(|arg| self.infer_from_ty(*arg)).collect(), + }, + TyKind::Function { params, ret } => InferTy::Function { + params: params + .iter() + .map(|param| self.infer_from_ty(*param)) + .collect(), + ret: Box::new(self.infer_from_ty(*ret)), + }, + TyKind::Tuple(elems) => { + InferTy::Tuple(elems.iter().map(|elem| self.infer_from_ty(*elem)).collect()) + } + TyKind::Comptime(inner) => InferTy::Comptime(Box::new(self.infer_from_ty(*inner))), + } + } + + fn instantiate_ty(&mut self, ty: Ty<'db>, vars: &[InferTy<'db>]) -> InferTy<'db> { + match ty.kind(self.db) { + TyKind::BoundVar(var) => vars + .get(var.index as usize) + .cloned() + .unwrap_or(InferTy::Error), + TyKind::Error => InferTy::Error, + TyKind::Unknown => self.fresh_var(), + TyKind::Named { ctor, args } => InferTy::Named { + ctor: *ctor, + args: args + .iter() + .map(|arg| self.instantiate_ty(*arg, vars)) + .collect(), + }, + TyKind::Function { params, ret } => InferTy::Function { + params: params + .iter() + .map(|param| self.instantiate_ty(*param, vars)) + .collect(), + ret: Box::new(self.instantiate_ty(*ret, vars)), + }, + TyKind::Tuple(elems) => InferTy::Tuple( + elems + .iter() + .map(|elem| self.instantiate_ty(*elem, vars)) + .collect(), + ), + TyKind::Comptime(inner) => { + InferTy::Comptime(Box::new(self.instantiate_ty(*inner, vars))) + } + } + } + + fn instantiate_pred( + &mut self, + pred: Pred<'db>, + vars: &[InferTy<'db>], + source: ObligationSource<'db>, + ) -> InstantiatedPred<'db> { + match pred.kind(self.db) { + PredKind::InClass { class, main, args } => { + InstantiatedPred::Obligation(PendingObligation { + class: *class, + main: self.instantiate_ty(*main, vars), + args: args + .iter() + .map(|arg| self.instantiate_ty(*arg, vars)) + .collect(), + source, + }) + } + PredKind::Eq { lhs, rhs } => { + let lhs = self.instantiate_ty(*lhs, vars); + let rhs = self.instantiate_ty(*rhs, vars); + match self.unify(lhs, rhs) { + Ok(()) => InstantiatedPred::None, + Err(error) => { + InstantiatedPred::EqualityError(PendingEqualityError { source, error }) + } + } + } + PredKind::Error => InstantiatedPred::None, + } + } + + fn unify_inner( + &mut self, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> Result<(), UnifyError<'db>> { + let expected = self.resolve(expected); + let actual = self.resolve(actual); + match (expected, actual) { + (InferTy::Error, _) | (_, InferTy::Error) => Ok(()), + (InferTy::Unknown, _) | (_, InferTy::Unknown) => Ok(()), + (InferTy::Var(lhs), InferTy::Var(rhs)) if lhs == rhs => Ok(()), + (InferTy::Var(var), ty) | (ty, InferTy::Var(var)) => self.bind_var(var, ty), + (InferTy::BoundVar(lhs), InferTy::BoundVar(rhs)) if lhs == rhs => Ok(()), + ( + InferTy::Tuple(elems), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + ) + | ( + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + InferTy::Tuple(elems), + ) if elems.is_empty() && args.is_empty() => Ok(()), + (InferTy::Tuple(elems), rhs) => { + let lhs = product_infer_ty(elems); + self.unify_inner(lhs, rhs) + } + (lhs, InferTy::Tuple(elems)) => { + let rhs = product_infer_ty(elems); + self.unify_inner(lhs, rhs) + } + ( + InferTy::Named { + ctor: lhs_ctor, + args: lhs_args, + }, + InferTy::Named { + ctor: rhs_ctor, + args: rhs_args, + }, + ) if lhs_ctor == rhs_ctor && lhs_args.len() == rhs_args.len() => { + for (lhs, rhs) in lhs_args.into_iter().zip(rhs_args) { + self.unify_inner(lhs, rhs)?; + } + Ok(()) + } + ( + InferTy::Function { + params: lhs_params, + ret: lhs_ret, + }, + InferTy::Function { + params: rhs_params, + ret: rhs_ret, + }, + ) if lhs_params.len() == rhs_params.len() => { + for (lhs, rhs) in lhs_params.into_iter().zip(rhs_params) { + self.unify_inner(lhs, rhs)?; + } + self.unify_inner(*lhs_ret, *rhs_ret) + } + (InferTy::Comptime(lhs), InferTy::Comptime(rhs)) => self.unify_inner(*lhs, *rhs), + (InferTy::Comptime(lhs), rhs) => self.unify_inner(*lhs, rhs), + (lhs, InferTy::Comptime(rhs)) => self.unify_inner(lhs, *rhs), + (expected, actual) => Err(UnifyError::Mismatch { expected, actual }), + } + } + + fn bind_var(&mut self, var: TyVid<'db>, ty: InferTy<'db>) -> Result<(), UnifyError<'db>> { + let root = self.table.find(var); + let ty = self.resolve(ty); + if matches!(ty, InferTy::Var(other) if other == root) { + return Ok(()); + } + if self.occurs(root, ty.clone()) { + return Err(UnifyError::Occurs { var: root, ty }); + } + match ty { + InferTy::Var(other) => { + self.table.union(root, other); + Ok(()) + } + ty => match self.table.probe_value(root) { + VarValue::Known(existing) => self.unify_inner(existing, ty), + VarValue::Unknown => { + self.table.union_value(root, VarValue::Known(ty)); + Ok(()) + } + }, + } + } + + fn occurs(&mut self, var: TyVid<'db>, ty: InferTy<'db>) -> bool { + match self.resolve(ty) { + InferTy::Var(other) => self.table.find(other) == self.table.find(var), + InferTy::Named { args, .. } | InferTy::Tuple(args) => { + args.into_iter().any(|arg| self.occurs(var, arg)) + } + InferTy::Function { params, ret } => { + params.into_iter().any(|param| self.occurs(var, param)) || self.occurs(var, *ret) + } + InferTy::Comptime(inner) => self.occurs(var, *inner), + InferTy::Error | InferTy::Unknown | InferTy::BoundVar(_) => false, + } + } +} + +impl<'db> UnifyError<'db> { + pub(super) fn diagnostic( + self, + engine: &mut InferTable<'db>, + span: LabelSpan, + names: &[String], + ) -> TypeckDiagnostic { + match self { + UnifyError::Mismatch { expected, actual } => TypeckDiagnostic::Mismatch { + span, + expected: engine.display_with_names(expected, names), + actual: engine.display_with_names(actual, names), + }, + UnifyError::Occurs { var: _, ty } => TypeckDiagnostic::OccursCheck { + span, + var: "an inferred type".to_owned(), + ty: engine.display_with_names(ty, names), + }, + } + } +} diff --git a/crates/hir-ty/src/infer/tests.rs b/crates/hir-ty/src/infer/tests.rs new file mode 100644 index 00000000..2502a465 --- /dev/null +++ b/crates/hir-ty/src/infer/tests.rs @@ -0,0 +1,3115 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, +}; + +use hir::{ + anchor::{DefId, DefLocationTable}, + ast::{ + function::{ExprKind, FuncParam, FuncSig, StmtKind}, + item::{ContractItem, FunctionDef, Item, Module}, + }, + input::SourceFile, + nameres::{self as hir_nameres, ident_text, type_var_bindings}, + sema::ty::QualTy, +}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, module_key_for_path, +}; +use parser::parse_file_to_hir; +use salsa::Setter; + +use super::*; +use crate::{ + BinderEnv, ClauseOrigin, Solution, TraitEnvId, TypeLowering, UserTyCtor, UserTyCtorKind, + canonical_goal, solve, solve_report, trait_env_for_module, trait_env_from_module_resolution, + trait_env_from_module_resolution_and_imports, trait_env_with_givens, +}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, + module_file_snapshot: Option, + module_files: FxHashMap, +} + +impl TestDb { + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + ModuleTree::new( + self, + PathBuf::from("/main"), + PathBuf::from("/std"), + BTreeMap::new(), + ) + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + ModuleFsSnapshot::new(self, BTreeSet::new(), BTreeMap::new()) + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .unwrap_or_else(|| ModuleFileSnapshot::new(self, BTreeMap::new())) + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl crate::Db for TestDb {} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn source_file_at_path(db: &TestDb, path: &std::path::Path, src: &str) -> SourceFile { + let url = url::Url::from_file_path(path).expect("file url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn parse_module<'db>(db: &'db TestDb, src: &str) -> Module<'db> { + parse_file_to_hir(db, source_file(db, "hir_ty", src)).module(db) +} + +fn module_key(path: &[&str]) -> ModuleKey { + ModuleKey { + library: LibraryId::Main, + logical_path: path.iter().map(|segment| (*segment).to_owned()).collect(), + } +} + +fn insert_module_source(db: &mut TestDb, path: &[&str], src: &str) -> ModuleKey { + let key = module_key(path); + let url = format!("memory:///{}.solc", path.join("/")) + .parse() + .expect("valid url"); + let file = SourceFile::new(&*db, url, Some(src.to_owned())); + db.insert_module_file(key.clone(), file); + key +} + +fn db_with_main_typeck(src: &str) -> (TestDb, ModuleKey) { + let mut db = TestDb::default(); + let key = insert_module_source(&mut db, &["main"], src); + (db, key) +} + +fn lowered_module_typeck_diagnostics(src: &str) -> Vec { + let (db, key) = db_with_main_typeck(src); + let module = module_id_from_key(&db, &key); + module_typeck_diagnostics(&db, module) + .iter() + .map(|diagnostic| diagnostic.lower(&db)) + .collect() +} + +fn function_name<'db>(db: &'db TestDb, function: FunctionDef<'db>) -> &'db str { + (*function.sig(db).name.atom()).text(db) +} + +fn sig_type_vars<'db>( + owner: DefId<'db>, + sig: &FuncSig<'db>, +) -> Vec> { + type_var_bindings(owner, &sig.type_vars) +} + +fn param_names<'db>(db: &'db TestDb, params: &[FuncParam<'db>]) -> Vec { + params + .iter() + .filter_map(|param| match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some(ident_text(db, name)) + } + FuncParam::Error { .. } => None, + }) + .collect() +} + +#[derive(Clone)] +struct FunctionInfo<'db> { + function: FunctionDef<'db>, + type_vars: Vec>, +} + +fn function_infos<'db>(db: &'db TestDb, module: Module<'db>) -> Vec> { + let mut infos = Vec::new(); + for item in module.items(db) { + collect_function_infos(db, *item, &[], &mut infos); + } + infos +} + +fn collect_function_infos<'db>( + db: &'db TestDb, + item: Item<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + infos: &mut Vec>, +) { + match item { + Item::FunctionDef(function) => push_function_info(db, function, inherited, infos), + Item::InstanceDef(instance) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + )); + for method in instance.methods(db) { + push_function_info(db, *method, &inherited, infos); + } + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) => { + push_function_info(db, function, &inherited, infos) + } + ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +fn push_function_info<'db>( + db: &'db TestDb, + function: FunctionDef<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + infos: &mut Vec>, +) { + let mut type_vars = inherited.to_vec(); + type_vars.extend(sig_type_vars(function.def_id_value(db), function.sig(db))); + infos.push(FunctionInfo { + function, + type_vars, + }); +} + +fn body_map<'db>( + db: &'db TestDb, + module_resolution: &hir_nameres::ModuleResolutionMap<'db>, + body: FuncBody<'db>, +) -> hir_nameres::BodyResolutionMap<'db> { + module_resolution + .bodies + .iter() + .find(|map| { + map.exprs.iter().any(|entry| entry.body == body) + || map.stmt_bindings.iter().any(|entry| entry.body == body) + || map.pats.iter().any(|entry| entry.body == body) + }) + .cloned() + .unwrap_or_else(|| { + // Bodies with no resolvable names (e.g. only literals) have no + // entries to match on; an empty map is the correct fallback. + let _ = db; + hir_nameres::BodyResolutionMap::default() + }) +} + +fn trait_env<'db>( + db: &'db TestDb, + module: Module<'db>, + module_resolution: &hir_nameres::ModuleResolutionMap<'db>, +) -> TraitEnvId<'db> { + trait_env_from_module_resolution(db, module, module_resolution) +} + +fn infer_function<'db>( + db: &'db TestDb, + module: Module<'db>, + name: &str, +) -> (FuncBody<'db>, InferenceResult<'db>) { + let info = function_infos(db, module) + .into_iter() + .find(|info| function_name(db, info.function) == name) + .expect("function"); + let function = info.function; + let body = function.body(db).expect("body"); + let module_resolution = hir_nameres::resolve_module(db, module); + let lowered = TypeLowering::from_item_resolutions( + db, + &module_resolution.item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_function(function); + let body_map = body_map(db, &module_resolution, body); + let ctx = BodyTyContext::new( + module, + body_map, + info.type_vars, + lowered.params, + Some(lowered.ret), + ) + .with_param_names(param_names(db, function.sig(db).params.atom())); + (body, infer_body(db, body, ctx)) +} + +fn infer_function_with_imports<'db>( + db: &'db TestDb, + module_id: ModuleId<'db>, + module: Module<'db>, + name: &str, +) -> (FuncBody<'db>, InferenceResult<'db>) { + let info = function_infos(db, module) + .into_iter() + .find(|info| function_name(db, info.function) == name) + .expect("function"); + let function = info.function; + let body = function.body(db).expect("body"); + let env = nameres::module_env_for_hir_module(db, module_id, module); + let scope = env.item_scope.clone().expect("item scope"); + let module_resolution = hir_nameres::resolve_module_with_imports(db, module, scope, &env); + let lowered = TypeLowering::from_item_resolutions( + db, + &module_resolution.item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_function(function); + let body_map = body_map(db, &module_resolution, body); + let ctx = BodyTyContext::new( + module, + body_map, + info.type_vars, + lowered.params, + Some(lowered.ret), + ) + .with_param_names(param_names(db, function.sig(db).params.atom())) + .with_entry_module(module_id); + (body, infer_body(db, body, ctx)) +} + +fn infer_all_functions_with_solver<'db>( + db: &'db TestDb, + module: Module<'db>, +) -> Vec<(String, InferenceResult<'db>)> { + let module_resolution = hir_nameres::resolve_module(db, module); + let base_trait_env = trait_env(db, module, &module_resolution); + function_infos(db, module) + .into_iter() + .filter_map(|info| { + let body = info.function.body(db)?; + let lowered = TypeLowering::from_item_resolutions( + db, + &module_resolution.item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ) + .lower_function(info.function); + let body_map = body_map(db, &module_resolution, body); + let trait_env = trait_env_with_givens( + db, + base_trait_env, + lowered.scheme.body(db).preds(db).clone(), + ); + let ctx = BodyTyContext::new( + module, + body_map, + info.type_vars, + lowered.params, + Some(lowered.ret), + ) + .with_param_names(param_names(db, info.function.sig(db).params.atom())) + .with_trait_env(trait_env); + Some(( + function_name(db, info.function).to_owned(), + infer_body(db, body, ctx), + )) + }) + .collect() +} + +fn class_id<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> ClassId<'db> { + for item in module.items(db) { + if let Item::ClassDef(class) = item + && class.def_id_value(db).name(db).as_deref() == Some(name) + { + return ClassId::User(class.def_id_value(db)); + } + } + panic!("trait {name}"); +} + +fn adt_def<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> DefId<'db> { + for item in module.items(db) { + if let Item::AdtDef(adt) = item + && adt.def_id_value(db).name(db).as_deref() == Some(name) + { + return adt.def_id_value(db); + } + } + panic!("adt {name}"); +} + +fn adt_ty<'db>(db: &'db TestDb, module: Module<'db>, name: &str, args: Vec>) -> Ty<'db> { + Ty::named( + db, + TyCtor::User(UserTyCtor { + def: adt_def(db, module, name), + kind: UserTyCtorKind::Adt, + }), + args, + ) +} + +fn solve_class_goal<'db>( + db: &'db TestDb, + env: TraitEnvId<'db>, + class: ClassId<'db>, + main: Ty<'db>, + args: Vec>, +) -> Solution<'db> { + let goal = Pred::in_class(db, class, main, args); + solve(db, env, canonical_goal(db, goal)) +} + +fn solve_class_report<'db>( + db: &'db TestDb, + env: TraitEnvId<'db>, + class: ClassId<'db>, + main: Ty<'db>, + args: Vec>, +) -> crate::SolverReport<'db> { + let goal = Pred::in_class(db, class, main, args); + solve_report(db, env, canonical_goal(db, goal)) +} + +fn return_expr<'db>(db: &'db TestDb, body: FuncBody<'db>) -> Id> { + let stmt = body.stmts(db).get(body.top_level_stmts(db)[0]); + match &stmt.kind { + StmtKind::Return(Some(expr)) => *expr, + _ => panic!("expected return expression"), + } +} + +#[test] +fn obligation_canonicalization_keeps_rigid_and_goal_variables_disjoint() { + let db = TestDb::default(); + let mut table = InferTable::new(&db); + let open = table.fresh_var(); + let mut canonicalizer = ObligationCanonicalizer::new(&db, &mut table, 1); + + let rigid = canonicalizer.ty(InferTy::BoundVar(0)); + let goal = canonicalizer.ty(open); + + assert!(matches!(rigid.kind(&db), TyKind::BoundVar(var) if var.index == 0)); + assert!(matches!(goal.kind(&db), TyKind::BoundVar(var) if var.index == 1)); + assert_eq!(canonicalizer.allowed_vars(), vec![1]); +} + +#[test] +fn deferred_dependency_snapshots_follow_union_roots() { + let db = TestDb::default(); + let mut engine = InferTable::new(&db); + let first = engine.fresh_vid(); + let second = engine.fresh_vid(); + let unrelated = engine.fresh_vid(); + let first_before = engine.resolve(InferTy::Var(first)); + let second_before = engine.resolve(InferTy::Var(second)); + let unrelated_before = engine.resolve(InferTy::Var(unrelated)); + + engine + .unify(InferTy::Var(first), InferTy::Var(second)) + .unwrap(); + let InferTy::Var(current_root) = engine.resolve(InferTy::Var(first)) else { + panic!("union of two open inference variables must remain open"); + }; + let stale_root = if current_root == first { second } else { first }; + let stale_before = if stale_root == first { + first_before.clone() + } else { + second_before.clone() + }; + let current_before = if current_root == first { + first_before + } else { + second_before + }; + assert_ne!(stale_root, current_root); + + // Do not inspect the snapshots between the union and the later binding. + // A dirty-root set containing only `current_root` would miss the snapshot + // keyed by `stale_root`, even though resolving that old handle follows the + // union and observes the concrete value. + engine + .unify( + InferTy::Var(current_root), + InferTy::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args: Vec::new(), + }, + ) + .unwrap(); + let deferred = FxHashMap::from_iter([ + (0, FxHashMap::from_iter([(stale_root, stale_before)])), + (1, FxHashMap::from_iter([(current_root, current_before)])), + (2, FxHashMap::from_iter([(unrelated, unrelated_before)])), + ]); + + assert_eq!( + deferred_obligations_affected_by(&mut engine, &deferred), + vec![0, 1] + ); +} + +fn function_info_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FunctionInfo<'db> { + function_infos(db, module) + .into_iter() + .find(|info| function_name(db, info.function) == name) + .expect("function") +} + +fn assert_no_typeck(result: &InferenceResult<'_>) { + assert!( + result.diagnostics.is_empty(), + "unexpected type diagnostics: {:?}", + result.diagnostics + ); +} + +#[test] +fn unannotated_function_scheme_uses_inferred_polymorphic_body_type() { + let db = TestDb::default(); + let module = parse_module(&db, "function id(x) { return x; }"); + let info = function_info_named(&db, module, "id"); + let scheme = function_scheme_in_hir_module(&db, module, info.function.def_id_value(&db)) + .expect("scheme"); + + assert_eq!(scheme.binder_count(&db), 1); + let TyKind::Function { params, ret } = scheme.body(&db).ty(&db).kind(&db) else { + panic!("expected function scheme"); + }; + assert_eq!(params.len(), 1); + assert!(matches!( + params[0].kind(&db), + TyKind::BoundVar(var) if var.index == 0 + )); + assert!(matches!( + ret.kind(&db), + TyKind::BoundVar(var) if var.index == 0 + )); +} + +#[test] +fn contract_entry_dispatch_uses_explicit_return_type() { + let mut db = TestDb::default(); + let key = insert_module_source( + &mut db, + &["main"], + r#" +contract Answer { + function main() public returns (word) { +return 42; + } +} +"#, + ); + let module = module_id_from_key(&db, &key); + let hir_module = module_hir(&db, module).expect("module hir"); + let contract = hir_module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + let surface = crate::contract_dispatch_surface(&db, hir_module, contract); + + assert_eq!(surface.methods.len(), 1); + assert_eq!(surface.methods[0].outputs.len(), 1); + assert_eq!(surface.methods[0].outputs[0].ty.to_string(), "uint256"); +} + +#[test] +fn inference_result_records_comptime_obligation_sites() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function need(comptime x: word) returns (comptime word) { + return x; +} + +function g() returns (comptime word) { + let comptime y: word = need(2); + return y; +} + +function f(x: word) returns (comptime word) { + match (x) { case comptime 1 { return need(2); } default { return 0; } } +} +"#, + ); + let (_, g_result) = infer_function(&db, module, "g"); + + assert!( + g_result + .comptime_obligations + .iter() + .any(|obligation| matches!(obligation.kind, ComptimeObligationKind::LetInit { .. })), + "{:?}", + g_result.comptime_obligations + ); + assert!( + g_result + .comptime_obligations + .iter() + .any(|obligation| matches!(obligation.kind, ComptimeObligationKind::CallParam { .. })), + "{:?}", + g_result.comptime_obligations + ); + assert!( + g_result + .comptime_obligations + .iter() + .any(|obligation| matches!(obligation.kind, ComptimeObligationKind::Return { .. })), + "{:?}", + g_result.comptime_obligations + ); + + let (_, f_result) = infer_function(&db, module, "f"); + assert!( + f_result + .comptime_obligations + .iter() + .any(|obligation| matches!( + obligation.kind, + ComptimeObligationKind::PatternLabel { .. } + )), + "{:?}", + f_result.comptime_obligations + ); +} + +#[test] +fn inferred_integer_let_records_comptime_obligation() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function f() returns (word) { + let x = wordToInteger(20); + return wordFromInteger(x); +} +"#, + ); + let (_, result) = infer_function(&db, module, "f"); + + assert!( + result + .comptime_obligations + .iter() + .any(|obligation| matches!( + &obligation.kind, + ComptimeObligationKind::LetInit { name, .. } if name == "x" + )), + "{:?}", + result.comptime_obligations + ); +} + +#[test] +fn unify_occurs_check_rejects_recursive_type() { + let db = TestDb::default(); + let mut table = InferTable::new(&db); + let var = table.fresh_vid(); + let recursive = InferTy::Function { + params: vec![InferTy::Var(var)], + ret: Box::new(table.from_ty(Ty::word(&db))), + }; + + let err = table + .unify(InferTy::Var(var), recursive) + .expect_err("occurs"); + assert!(matches!(err, UnifyError::Occurs { .. })); +} + +#[test] +fn unify_trial_rolls_back_successful_snapshot() { + let db = TestDb::default(); + let mut table = InferTable::new(&db); + let var = table.fresh_vid(); + let word = table.from_ty(Ty::word(&db)); + + assert!(table.can_unify(InferTy::Var(var), word.clone())); + assert_eq!(table.ground_ty(InferTy::Var(var)), Ty::unknown(&db)); + + table + .unify(InferTy::Var(var), word) + .expect("committed unify"); + assert_eq!(table.ground_ty(InferTy::Var(var)), Ty::word(&db)); +} + +#[test] +fn scheme_instantiation_reuses_one_fresh_var_per_binder() { + let db = TestDb::default(); + let bound = Ty::bound(&db, 0); + let scheme = TyScheme::new( + &db, + 1, + QualTy::monotype(&db, Ty::function(&db, vec![bound], bound)), + ); + let mut table = InferTable::new(&db); + let instantiated = table.instantiate_scheme(scheme); + + let InferTy::Function { params, ret } = instantiated.ty else { + panic!("function scheme"); + }; + let InferTy::Var(param_var) = ¶ms[0] else { + panic!("fresh param var"); + }; + let InferTy::Var(ret_var) = &*ret else { + panic!("fresh ret var"); + }; + assert_eq!(param_var, ret_var); +} + +#[test] +fn ambiguous_integer_literal_defaults_to_word() { + let db = TestDb::default(); + let module = parse_module(&db, "function f() returns (word) { return 1; }"); + let (body, result) = infer_function(&db, module, "f"); + assert!(result.diagnostics.is_empty()); + + let expr = return_expr(&db, body); + assert_eq!(result.expr_ty(body, expr), Some(Ty::word(&db))); + assert_eq!(result.obligations.len(), 1); + assert_eq!(result.obligations[0].pred.display(&db), "word: Int"); +} + +#[test] +fn end_to_end_body_infers_word_arithmetic() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Add { + function add(l: t, r: t) returns (t) ; +} + +impl Add { + function add(l: word, r: word) returns (word) { +return primAddWord(l, r); + } +} + +function f(x: word) returns (word) { return x + 1; } +"#, + ); + let (body, result) = infer_function(&db, module, "f"); + assert!(result.diagnostics.is_empty()); + + let expr = return_expr(&db, body); + assert!(matches!( + &body.exprs(&db).get(expr).kind, + ExprKind::BinOp { + op, + .. + } if *op.atom() == BinOp::Add + )); + assert_eq!(result.expr_ty(body, expr), Some(Ty::word(&db))); + assert!( + result + .obligations + .iter() + .any(|obligation| obligation.pred.display(&db) == "word: Int"), + "{:?}", + result.obligations + ); +} + +#[test] +fn trait_method_call_emits_obligation() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Enum { + function fromEnum(x: a) returns (word) ; +} + +enum Food { Curry, Beans, Other } + +function main() returns (word) { + return Enum.fromEnum(Food.Beans); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + assert_no_typeck(&result); + let obligations = result + .obligations + .iter() + .map(|obligation| obligation.pred.display(&db)) + .collect::>(); + assert!( + obligations + .iter() + .any(|obligation| obligation.contains("trait:Enum")), + "expected Enum obligation, got {obligations:?}", + ); +} + +#[test] +fn pair_domains_preserve_source_call_arity_and_explicit_tuple_arguments() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function call_zero(f: function() returns (word)) returns (word) { + return f(); +} + +function call_pair(f: function(word, bool) returns (word), x: word, y: bool) returns (word) { + return f(x, y); +} + +function call_tuple(f: function((word, bool)) returns (word), x: (word, bool)) returns (word) { + return f(x); +} +"#, + ); + + for (name, result) in infer_all_functions_with_solver(&db, module) { + assert!( + result.diagnostics.is_empty(), + "{name}: {:?}", + result.diagnostics + ); + assert!( + result.checked_conversions.is_empty(), + "{name}: rejected assignment conversions must not reach specialization" + ); + } +} + +#[test] +fn call_diagnostics_preserve_qualified_function_types_without_a_return_clause() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function use(comptime callback: function(word) external view returns (bool)) {} + +function main() { + use(); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + let diagnostic = result + .diagnostics + .iter() + .find(|diagnostic| matches!(diagnostic, TypeckDiagnostic::WrongArity { .. })) + .expect("wrong-arity diagnostic"); + let TypeckDiagnostic::WrongArity { + callee: Some(callee), + .. + } = diagnostic + else { + panic!("wrong-arity diagnostic should retain its callee"); + }; + assert_eq!( + callee.signature, + "use(comptime callback: function(word) external view returns (bool))" + ); +} + +#[test] +fn constructor_call_diagnostics_preserve_qualified_function_payloads() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum CallbackBox { CallbackBox(function(word) external view returns (bool)) } + +function main() { + CallbackBox.CallbackBox(true); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + let diagnostic = result + .diagnostics + .iter() + .find(|diagnostic| matches!(diagnostic, TypeckDiagnostic::ArgMismatch { .. })) + .unwrap_or_else(|| panic!("argument-mismatch diagnostic: {:?}", result.diagnostics)); + let TypeckDiagnostic::ArgMismatch { + callee: Some(callee), + param, + .. + } = diagnostic + else { + panic!("argument-mismatch diagnostic should retain constructor metadata"); + }; + assert_eq!( + callee.signature, + "CallbackBox(function(word) external view returns (bool)) returns (CallbackBox)" + ); + assert_eq!( + param.ty.as_deref(), + Some("function(word) external view returns (bool)") + ); +} + +#[test] +fn tuple_destructuring_let_resolves_bindings_and_typechecks() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function pair(left: word, right: word) returns (word, word) { + return (left, right); +} + +function first(left: word, right: word) returns (word) { + return left; +} + +function main() returns (word) { + let (left, right): (word, word) = pair(1, 2); + return first(left, right); +} +"#, + ); + + let resolution = hir_nameres::resolve_module(&db, module); + assert!(resolution.diagnostics.is_empty(), "{resolution:?}"); + for (name, result) in infer_all_functions_with_solver(&db, module) { + assert!( + result.diagnostics.is_empty(), + "{name}: {:?}", + result.diagnostics + ); + } +} + +#[test] +fn class_method_local_forall_is_lowered_as_a_method_binder() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait IsA { + function ais(p: (a, b)) returns (a) ; +} +"#, + ); + let resolution = hir_nameres::resolve_module(&db, module); + assert!(resolution.diagnostics.is_empty(), "{resolution:?}"); + let class = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ClassDef(class) => Some(*class), + _ => None, + }) + .expect("trait"); + let method = &class.methods(&db)[0]; + let method_type_vars = class_method_type_vars(&db, class, method); + let scheme = TypeLowering::from_item_resolutions( + &db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(&method_type_vars), + ) + .lower_class_method(class, method); + + assert_eq!(scheme.binder_count(&db), 2); + let TyKind::Function { params, ret } = scheme.body(&db).ty(&db).kind(&db) else { + panic!("method should lower to a function"); + }; + assert_eq!(params.len(), 1); + let TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } = params[0].kind(&db) + else { + panic!("method parameter should be a pair"); + }; + assert!(matches!(args[0].kind(&db), TyKind::BoundVar(var) if var.index == 1)); + assert!(matches!(args[1].kind(&db), TyKind::BoundVar(var) if var.index == 0)); + assert!(matches!(ret.kind(&db), TyKind::BoundVar(var) if var.index == 1)); +} + +#[test] +fn method_local_forall_survives_instance_signature_soundness() { + let diagnostics = lowered_module_typeck_diagnostics( + r#" +trait IsA { + function ais(x: a, witness: b) returns (a) ; +} + +impl IsA { + function ais(x: a, witness: word) returns (a) { + return x; + } +} +"#, + ); + + assert!(diagnostics.is_empty(), "{diagnostics:?}"); +} + +#[test] +fn fixed_array_lengths_survive_semantic_lowering_and_type_equality() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function same(x: word[4]) returns (word[4]) { + return x; +} + +function different(x: word[4]) returns (word[5]) { + return x; +} +"#, + ); + let same = function_info_named(&db, module, "same"); + let scheme = function_scheme_in_hir_module(&db, module, same.function.def_id_value(&db)) + .expect("fixed-array function scheme"); + let TyKind::Function { params, ret } = scheme.body(&db).ty(&db).kind(&db) else { + panic!("expected function scheme"); + }; + assert!(matches!( + params[0].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(4)), + args, + } if matches!( + args.as_slice(), + [element] if matches!( + element.kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } if args.is_empty() + ) + ) + )); + assert_eq!(params[0], *ret); + assert_eq!( + crate::display::display_ty_source(&db, params[0], &[]), + "word[4]" + ); + + let (_, same_result) = infer_function(&db, module, "same"); + assert_no_typeck(&same_result); + + let (_, different_result) = infer_function(&db, module, "different"); + assert!( + different_result + .diagnostics + .iter() + .any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::Mismatch { + expected, + actual, + .. + } if expected == "word[5]" && actual == "word[4]" + )) + ); +} + +#[test] +fn function_type_qualifiers_are_erased_only_at_semantic_lowering() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +alias InternalCallback = function(word) internal pure returns (bool); +alias ExternalCallback = function(word) external payable returns (bool); +"#, + ); + let aliases = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .collect::>(); + assert_eq!(aliases.len(), 2); + assert_ne!( + aliases[0].ty(&db).semantic_shape(), + aliases[1].ty(&db).semantic_shape(), + "source structural shapes must retain qualifier distinctions" + ); + + let resolution = hir_nameres::resolve_module(&db, module); + assert!(resolution.diagnostics.is_empty(), "{resolution:?}"); + let lowerer = TypeLowering::from_item_resolutions( + &db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(&[]), + ); + let internal = lowerer.lower_type_alias(aliases[0]).ty; + let external = lowerer.lower_type_alias(aliases[1]).ty; + + assert_eq!( + internal, external, + "the current checked-type model erases qualifiers only after source HIR retains them" + ); + assert!(matches!( + internal.kind(&db), + TyKind::Function { params, ret } + if params.len() == 1 + && matches!( + params[0].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } if args.is_empty() + ) + && matches!( + ret.kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + args, + } if args.is_empty() + ) + )); +} + +#[test] +fn comptime_numeric_scrutinees_accept_integer_literal_patterns() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function classify_word(comptime x: word) returns (word) { + match (x) { case 0 { return 10; } default { return 20; } } +} + +function classify_integer(comptime x: integer) returns (word) { + match (x) { case 0 { return 10; } default { return 20; } } +} +"#, + ); + + for (name, result) in infer_all_functions_with_solver(&db, module) { + assert!( + result.diagnostics.is_empty(), + "{name}: {:?}", + result.diagnostics + ); + } +} + +#[test] +fn surface_conversion_accepts_only_alias_normalized_identity() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +alias WordAlias = word; +alias WordAlias2 = WordAlias; + +function generic_identity(x: a) returns (a) { + return x as a; +} + +function alias_identity(x: WordAlias2) returns (word) { + return x as word; +} +"#, + ); + + for name in ["generic_identity", "alias_identity"] { + let (_, result) = infer_function(&db, module, name); + assert_no_typeck(&result); + assert_eq!(result.checked_conversions.len(), 1, "{name}: {result:?}"); + assert_eq!(result.checked_conversions[0].kind, ConversionKind::Identity); + } +} + +#[test] +fn shorthand_pattern_expands_contract_alias_with_captured_type_var() { + let diagnostics = lowered_module_typeck_diagnostics( + r#" +contract C { + enum Either { Left(a), Right(b) } + alias Captured = Either; + + function unwrap(x: Captured, fallback: t) returns (t) { + match (x) { + case Either.Left(value) { return value; } + default { return fallback; } + } + } +} +"#, + ); + + assert!(diagnostics.is_empty(), "{diagnostics:?}"); +} + +#[test] +fn value_type_conversions_are_nominal_wraps_and_unwraps() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +alias WordAlias = word; +type Wad is WordAlias; +type Price is word; + +function wrap(x: WordAlias) returns (Wad) { + return x as Wad; +} + +function unwrap(x: Wad) returns (word) { + return x as word; +} + +function direct(x: Wad) returns (Price) { + return x as Price; +} + +function staged(x: Wad) returns (Price) { + return (x as word) as Price; +} + +function implicit(x: word) returns (Wad) { + return x; +} +"#, + ); + + let (_, wrap) = infer_function(&db, module, "wrap"); + assert_no_typeck(&wrap); + assert_eq!( + wrap.checked_conversions[0].kind, + ConversionKind::ValueTypeWrap + ); + + let (_, unwrap) = infer_function(&db, module, "unwrap"); + assert_no_typeck(&unwrap); + assert_eq!( + unwrap.checked_conversions[0].kind, + ConversionKind::ValueTypeUnwrap + ); + + let (_, direct) = infer_function(&db, module, "direct"); + assert!(direct.checked_conversions.is_empty()); + assert!(direct.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::InvalidConversion { source, target, .. } + if source == "Wad" && target == "Price" + ))); + + let (_, staged) = infer_function(&db, module, "staged"); + assert_no_typeck(&staged); + assert_eq!( + staged + .checked_conversions + .iter() + .map(|conversion| conversion.kind) + .collect::>(), + vec![ + ConversionKind::ValueTypeUnwrap, + ConversionKind::ValueTypeWrap, + ] + ); + + let (_, implicit) = infer_function(&db, module, "implicit"); + assert!(implicit.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::Mismatch { expected, actual, .. } + if expected == "Wad" && actual == "word" + ))); +} + +#[test] +fn invalid_value_type_definitions_have_a_dedicated_diagnostic() { + for source in [ + "type Generic is word;", + "type FunctionBacked is function(word) returns (word);", + "type TupleBacked is (word, word);", + "type First is word; type Nested is First;", + "enum uint256 { uint256(word) } type Fake is uint256;", + "contract Generic { type Inner is word; }", + ] { + let diagnostics = lowered_module_typeck_diagnostics(source); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0247")), + "{source}: {diagnostics:#?}" + ); + } + + let diagnostics = + lowered_module_typeck_diagnostics("type Wad is word; function bad(x: Wad) {}"); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0299")), + "{diagnostics:#?}" + ); +} + +#[test] +fn imported_value_type_underlying_supports_checked_conversions() { + let mut db = TestDb::default(); + let types_key = insert_module_source(&mut db, &["types"], "export { Wad }; type Wad is word;"); + let main_key = insert_module_source( + &mut db, + &["main"], + r#" +import { Wad } from types; + +function wrap(x: word) returns (Wad) { + return x as Wad; +} + +function unwrap(x: Wad) returns (word) { + return x as word; +} +"#, + ); + let _types_module = module_id_from_key(&db, &types_key); + let main_module = module_id_from_key(&db, &main_key); + let main_file = nameres::Db::module_file(&db, main_module).expect("main source file"); + let module = parse_file_to_hir(&db, main_file).module(&db); + + let (_, wrap) = infer_function_with_imports(&db, main_module, module, "wrap"); + assert_no_typeck(&wrap); + assert_eq!( + wrap.checked_conversions[0].kind, + ConversionKind::ValueTypeWrap + ); + + let (_, unwrap) = infer_function_with_imports(&db, main_module, module, "unwrap"); + assert_no_typeck(&unwrap); + assert_eq!( + unwrap.checked_conversions[0].kind, + ConversionKind::ValueTypeUnwrap + ); +} + +#[test] +fn invalid_surface_conversion_has_dedicated_diagnostic() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function bad(x: word) returns (bool) { return x as bool; }", + ); + let (_, result) = infer_function(&db, module, "bad"); + + assert_eq!( + result + .diagnostics + .iter() + .filter(|diagnostic| matches!(diagnostic, TypeckDiagnostic::InvalidConversion { .. })) + .count(), + 1, + "{:?}", + result.diagnostics + ); + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| !matches!(diagnostic, TypeckDiagnostic::Mismatch { .. })), + "{:?}", + result.diagnostics + ); + assert!(result.checked_conversions.is_empty()); +} + +#[test] +fn unresolved_surface_conversion_suppresses_cascade_diagnostic() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function unresolved(x) returns (word) { return x as word; }", + ); + let (_, result) = infer_function(&db, module, "unresolved"); + + assert!( + result + .diagnostics + .iter() + .all(|diagnostic| !matches!(diagnostic, TypeckDiagnostic::InvalidConversion { .. })), + "{:?}", + result.diagnostics + ); + assert!(result.checked_conversions.is_empty()); +} + +#[test] +fn comptime_wrapper_is_not_an_identity_conversion() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function bad(comptime x: word) returns (word) { return x as word; }", + ); + let (_, result) = infer_function(&db, module, "bad"); + assert!(result.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::InvalidConversion { source, target, .. } + if source == "comptime word" && target == "word" + ))); +} + +#[test] +fn conversion_is_not_an_assignment_location() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +struct Box { value: word; } + +function direct(x: word) { + (x as word) = 1; +} + +function through_field(x: Box) { + (x as Box).value = 1; +} +"#, + ); + for name in ["direct", "through_field"] { + let (_, result) = infer_function(&db, module, name); + assert!( + result.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::ConversionAssignmentTarget { .. } + )), + "{name}: {:?}", + result.diagnostics + ); + } +} + +#[test] +fn unconstrained_phantom_constructor_result_is_ambiguous() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Foo { Foo(word) } + +function read(x: Foo) returns (word) { + return 0; +} + +function main() returns (word) { + return read(Foo.Foo(42)); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + + assert!( + result + .diagnostics + .iter() + .any(|diagnostic| matches!(diagnostic, TypeckDiagnostic::AmbiguousInferredType { .. })), + "{:?}", + result.diagnostics + ); +} + +#[test] +fn payload_constrained_constructor_result_is_not_phantom() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Box { Box(a) } + +function unwrap(x: Box) returns (a) { + match (x) { case Box.Box(value) { return value; } } +} + +function main() returns (word) { + return unwrap(Box.Box(42)); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + + assert_no_typeck(&result); +} + +#[test] +fn expected_type_constrains_phantom_constructor_result() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Foo { Foo(word) } + +function main() returns (Foo) { + return Foo.Foo(42); +} +"#, + ); + let (_, result) = infer_function(&db, module, "main"); + + assert_no_typeck(&result); +} + +#[test] +fn named_struct_field_access_infers_its_generic_field_type() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +struct Pair { + x: T; + flag: bool; +} + +function project(p: Pair) returns (word) { + return p.x; +} +"#, + ); + let (body, result) = infer_function(&db, module, "project"); + + assert_no_typeck(&result); + let expr = return_expr(&db, body); + assert_eq!(result.expr_ty(body, expr), Some(Ty::word(&db))); + assert!(result.adt_field_selections.iter().any(|selection| { + selection.body == body && selection.expr == expr && selection.index == 0 + })); +} + +#[test] +fn unknown_named_struct_field_is_diagnosed() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +struct Pair { + x: word; +} + +function project(p: Pair) returns (word) { + return p.missing; +} +"#, + ); + let (_, result) = infer_function(&db, module, "project"); + + assert!(result.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnknownField { field, .. } if field == "missing" + ) + })); + assert!(result.adt_field_selections.is_empty()); +} + +#[test] +fn contract_storage_struct_field_read_is_rejected_before_field_selection() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +struct Pair { + x: word; +} + +contract C { + pair: Pair; + + function get() returns (word) { + return pair.x; + } +} +"#, + ); + let (_, result) = infer_function(&db, module, "get"); + + let diagnostic = result + .diagnostics + .iter() + .find(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnsupportedStorageFieldProjection { field, .. } + if field == "x" + ) + }) + .expect("unsupported storage field projection diagnostic"); + assert_eq!( + diagnostic.lower().code.as_deref(), + Some(hir::diag::DiagnosticCode::TYPECK_UNSUPPORTED_STORAGE_FIELD_PROJECTION) + ); + assert!(result.adt_field_selections.is_empty()); +} + +#[test] +fn contract_storage_struct_field_assignment_is_rejected_before_field_selection() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +struct Pair { + x: word; +} + +contract C { + pair: Pair; + + function set(value: word) { + pair.x = value; + } +} +"#, + ); + let (_, result) = infer_function(&db, module, "set"); + + assert!(result.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnsupportedStorageFieldProjection { field, .. } + if field == "x" + ) + })); + assert!(result.adt_field_selections.is_empty()); +} + +#[test] +fn mapping_storage_struct_field_projection_is_rejected_before_field_selection() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum mapping { mapping(word) } + +struct Pair { + x: word; +} + +contract C { + pairs: mapping(word => Pair); + + function get(key: word) returns (word) { + return pairs[key].x; + } +} +"#, + ); + let (_, result) = infer_function(&db, module, "get"); + + assert!(result.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnsupportedStorageFieldProjection { field, .. } + if field == "x" + ) + })); + assert!(result.adt_field_selections.is_empty()); +} + +#[test] +fn storage_word_field_read_loads_as_word_without_context() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum storage { storage(word) } + +trait CanStore { + function store(r: a, v: b) returns () ; + function load(r: a) returns (b) ; +} + +impl CanStore { + function store(dst: word storage, src: word) returns () { +return (); + } + + function load(src: word storage) returns (word) { +return 0; + } +} + +contract C { + value: word; + + function get() returns (word) { +let x = value; +return x; + } +} +"#, + ); + let (body, result) = infer_function(&db, module, "get"); + assert_no_typeck(&result); + + let value_expr = body + .exprs(&db) + .iter() + .find_map(|(expr_id, expr)| match &expr.kind { + ExprKind::Ident(name) if (*name.atom()).text(&db) == "value" => Some(expr_id), + _ => None, + }) + .expect("value expression"); + assert_eq!(result.expr_ty(body, value_expr), Some(Ty::word(&db))); +} + +#[test] +fn storage_value_type_field_uses_its_underlying_representation() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum storage { storage(word) } +type Wad is word; + +contract C { + value: Wad; + + function get() returns (Wad) { + return value; + } + + function set(next: Wad) { + value = next; + } +} +"#, + ); + let (body, get) = infer_function(&db, module, "get"); + assert_no_typeck(&get); + let value_expr = body + .exprs(&db) + .iter() + .find_map(|(expr_id, expr)| match &expr.kind { + ExprKind::Ident(name) if (*name.atom()).text(&db) == "value" => Some(expr_id), + _ => None, + }) + .expect("value expression"); + let loaded = get.expr_ty(body, value_expr).expect("loaded value type"); + assert!(matches!( + loaded.kind(&db), + TyKind::Named { + ctor: TyCtor::User(UserTyCtor { + def, + kind: UserTyCtorKind::ValueType, + }), + args, + } if args.is_empty() && def.name(&db).as_deref() == Some("Wad") + )); + + let (_, set) = infer_function(&db, module, "set"); + assert_no_typeck(&set); +} + +#[test] +fn storage_rejects_non_word_value_type_representations() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum storage { storage(word) } +type Flag is bool; + +contract C { + value: Flag; + + function get() returns (Flag) { + return value; + } + + function set(next: Flag) { + value = next; + } +} +"#, + ); + + for function in ["get", "set"] { + let (_, result) = infer_function(&db, module, function); + assert!( + result.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::UnsupportedValueTypeStorage { ty, .. } if ty == "Flag" + )), + "{function}: {:?}", + result.diagnostics + ); + } +} + +#[test] +fn storage_rejects_fixed_arrays_with_a_dedicated_diagnostic() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum storage { storage(word) } + +contract C { + values: word[3]; + + function get() returns (word[3]) { + return values; + } + + function set(next: word[3]) { + values = next; + } +} +"#, + ); + + for function in ["get", "set"] { + let (_, result) = infer_function(&db, module, function); + let diagnostic = result + .diagnostics + .iter() + .find(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnsupportedFixedArrayStorage { ty, .. } if ty == "word[3]" + ) + }) + .unwrap_or_else(|| { + panic!( + "{function}: missing fixed-array storage diagnostic: {:?}", + result.diagnostics + ) + }); + let lowered = diagnostic.lower(); + assert_eq!( + lowered.code.as_deref(), + Some(hir::diag::DiagnosticCode::TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE) + ); + assert!( + lowered.message.contains("fixed-length array") + && lowered.message.contains("cannot be stored"), + "{function}: {lowered:?}" + ); + } +} + +#[test] +fn storage_rejects_fixed_arrays_hidden_in_nominal_adt_layouts() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum storage { storage(word) } + +struct Box { + values: word[3]; +} + +contract C { + boxed: Box; + + function get() returns (Box) { + return boxed; + } +} +"#, + ); + + let (_, result) = infer_function(&db, module, "get"); + let diagnostic = result + .diagnostics + .iter() + .find(|diagnostic| { + matches!( + diagnostic, + TypeckDiagnostic::UnsupportedFixedArrayStorage { ty, .. } if ty == "Box" + ) + }) + .unwrap_or_else(|| { + panic!( + "missing nominal fixed-array storage diagnostic: {:?}", + result.diagnostics + ) + }); + let lowered = diagnostic.lower(); + assert_eq!( + lowered.code.as_deref(), + Some(hir::diag::DiagnosticCode::TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE) + ); + assert!(lowered.message.contains("fixed-length array")); +} + +#[test] +fn storage_string_field_read_loads_as_memory_string_without_context() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum string {} +enum memory { memory(word) } +enum storage { storage(word) } + +trait CanStore { + function store(r: a, v: b) returns () ; + function load(r: a) returns (b) ; +} + +impl CanStore { + function store(dst: string storage, src: string memory) returns () { +return (); + } + + function load(src: string storage) returns (string memory) { +return memory.memory(0); + } +} + +contract C { + value: string; + + function get() returns (string memory) { +let x = value; +return x; + } +} +"#, + ); + let (body, result) = infer_function(&db, module, "get"); + assert_no_typeck(&result); + + let value_expr = body + .exprs(&db) + .iter() + .find_map(|(expr_id, expr)| match &expr.kind { + ExprKind::Ident(name) if (*name.atom()).text(&db) == "value" => Some(expr_id), + _ => None, + }) + .expect("value expression"); + let string_ty = adt_ty(&db, module, "string", Vec::new()); + let memory_string = adt_ty(&db, module, "memory", vec![string_ty]); + assert_eq!(result.expr_ty(body, value_expr), Some(memory_string)); +} + +#[test] +fn storage_mapping_assignment_records_concrete_base_ref_type() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum mapping { mapping(word) } +enum storage { storage(word) } + +trait CanStore { + function store(r: a, v: b) returns () ; + function load(r: a) returns (b) ; +} + +impl CanStore { + function store(dst: word storage, src: word) returns () { +return (); + } + + function load(src: word storage) returns (word) { +return 0; + } +} + +contract C { + m: mapping(word => word); + + function next() returns (word) { +return 1; + } + + function main() { +m[next()] = next(); + } +} +"#, + ); + let (body, result) = infer_function(&db, module, "main"); + assert_no_typeck(&result); + + let mapping_expr = body + .exprs(&db) + .iter() + .find_map(|(expr_id, expr)| match &expr.kind { + ExprKind::Ident(name) if (*name.atom()).text(&db) == "m" => Some(expr_id), + _ => None, + }) + .expect("mapping field expression"); + let word = Ty::word(&db); + let mapping = adt_ty(&db, module, "mapping", vec![word, word]); + let storage_mapping = adt_ty(&db, module, "storage", vec![mapping]); + assert_eq!(result.expr_ty(body, mapping_expr), Some(storage_mapping)); +} + +#[test] +fn constrained_function_call_records_call_site_evidence() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum T { T } + +trait C {} +impl C {} + +function use(x: a) returns (word) where a: C { return 0; } + +function main(t: T) returns (word) { + return use(t); +} +"#, + ); + let info = function_infos(&db, module) + .into_iter() + .find(|info| function_name(&db, info.function) == "main") + .expect("main function"); + let body = info.function.body(&db).expect("main body"); + let call_expr = return_expr(&db, body); + assert!(matches!( + body.exprs(&db).get(call_expr).kind, + ExprKind::Call { .. } + )); + + let result = infer_all_functions_with_solver(&db, module) + .into_iter() + .find(|(name, _)| name == "main") + .map(|(_, result)| result) + .expect("main result"); + + assert!( + result.call_site_evidence.iter().any(|evidence| { + evidence.body == body + && evidence.call_expr == call_expr + && matches!( + evidence.callee, + CallSiteCallee::Function(def) + if def.name(&db).as_deref() == Some("use") + ) + }), + "expected call-site evidence for use(t), got {:?}", + result.call_site_evidence + ); +} + +#[test] +fn trait_solver_rejects_unproductive_instance_cycle() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait C {} +impl C where a: C {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "C"), + Ty::word(&db), + Vec::new(), + ); + assert!(matches!(solution, Solution::NoSolution)); +} + +#[test] +fn tabled_solver_cycle_saturates_without_fuel_diagnostic() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait C {} +impl C where a: C {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let report = solve_class_report( + &db, + env, + class_id(&db, module, "C"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!(report.solution, Solution::NoSolution)); + assert!(!report.exhausted, "{report:?}"); + + let diagnostics = lowered_module_typeck_diagnostics( + r#" +pragma solcore noPattersonCondition C; + +trait C {} + +impl C where a: C {} + +function needsC(x: a) returns () where a: C { + return (); +} + +function main(x: word) returns () { + return needsC(x); +} +"#, + ); + assert!( + diagnostics + .iter() + .all(|diagnostic| diagnostic.code.as_deref() != Some("SC0209")), + "{diagnostics:?}" + ); +} + +#[test] +fn tabled_solver_mutual_recursion_saturates_without_answers() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait C {} +trait D {} + +impl C where a: D {} +impl D where a: C {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "C"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!(report.solution, Solution::NoSolution)); + assert!(!report.exhausted, "{report:?}"); + assert_eq!(report.stats.answers_found, 0, "{report:?}"); +} + +#[test] +fn tabled_solver_shares_diamond_subgoals() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Leaf {} +trait Left {} +trait Right {} +trait Top {} + +impl Leaf {} + +impl Left where a: Leaf {} +impl Right where a: Leaf {} +impl Top where a: Left, a: Right {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Top"), + Ty::word(&db), + Vec::new(), + ); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert!(!report.exhausted, "{report:?}"); + assert_eq!(report.stats.table_size, 4, "{report:?}"); + assert_eq!(report.stats.answers_found, 4, "{report:?}"); +} + +#[test] +fn tabled_solver_shares_alpha_equivalent_flexible_subgoals() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Pair { Pair(a, b) } + +trait Leaf {} +trait Left {} +trait Right {} +trait Top {} + +impl Leaf {} + +impl Left where Pair: Leaf {} +impl Right where Pair: Leaf {} +impl Top where a: Left, a: Right {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Top"), + Ty::word(&db), + Vec::new(), + ); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert!(!report.exhausted, "{report:?}"); + // The Left and Right clauses allocate their two Leaf variables in + // opposite numeric order, but both conditions canonicalize to one table. + assert_eq!(report.stats.table_size, 4, "{report:?}"); +} + +#[test] +fn tabled_solver_dedups_replayed_identical_answer() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Seed {} +trait Derived {} + +impl Seed {} + +impl Derived where a: Seed, a: Seed {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Derived"), + Ty::word(&db), + Vec::new(), + ); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert_eq!(report.stats.table_size, 2, "{report:?}"); + assert_eq!(report.stats.answers_found, 2, "{report:?}"); +} + +#[test] +fn tabled_solver_replays_answers_to_late_consumers() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Seed {} +trait Derived {} +trait Needs {} + +impl Seed {} + +impl Derived where a: Seed {} +impl Needs where a: Seed, a: Derived {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Needs"), + Ty::word(&db), + Vec::new(), + ); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert_eq!(report.stats.table_size, 3, "{report:?}"); + assert_eq!(report.stats.answers_found, 3, "{report:?}"); +} + +#[test] +fn trait_solver_resolves_recursive_pair_instance() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Pair { Pair(a, b) } + +trait StorageSize {} + +impl StorageSize {} + +impl StorageSize> where a: StorageSize, b: StorageSize {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let word = Ty::word(&db); + let pair_word_word = adt_ty(&db, module, "Pair", vec![word, word]); + let nested = adt_ty(&db, module, "Pair", vec![pair_word_word, word]); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "StorageSize"), + nested, + Vec::new(), + ); + + let Solution::Unique { evidence, .. } = solution else { + panic!("expected unique solution, got {solution:?}"); + }; + let Evidence::Instance { sub_evidence, .. } = evidence else { + panic!("expected impl evidence"); + }; + assert_eq!(sub_evidence.len(), 2); + assert!(matches!(sub_evidence[0], Evidence::Instance { .. })); + assert!(matches!(sub_evidence[1], Evidence::Instance { .. })); +} + +#[test] +fn trait_solver_prefilters_only_heads_that_cannot_unify() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Target {} +trait Noise {} +trait DefaultTarget {} +trait GenericTarget {} +trait GivenTarget {} +trait Parent {} +trait Child where a: Parent {} +trait AmbiguousTarget {} + +impl Target {} +impl Noise {} +default impl Noise {} +default impl DefaultTarget {} +impl GenericTarget {} +impl AmbiguousTarget {} +impl AmbiguousTarget {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let base_env = trait_env(&db, module, &module_resolution); + let word = Ty::word(&db); + let env = trait_env_with_givens( + &db, + base_env, + vec![ + Pred::in_class(&db, class_id(&db, module, "Noise"), word, Vec::new()), + Pred::in_class(&db, class_id(&db, module, "Child"), word, Vec::new()), + Pred::in_class(&db, class_id(&db, module, "GivenTarget"), word, Vec::new()), + ], + ); + + let target = solve_class_report(&db, env, class_id(&db, module, "Target"), word, Vec::new()); + assert!( + matches!(target.solution, Solution::Unique { .. }), + "{target:?}" + ); + assert_eq!(target.stats.generator_steps, 1, "{target:?}"); + + let generic = solve_class_report( + &db, + env, + class_id(&db, module, "GenericTarget"), + word, + Vec::new(), + ); + assert!( + matches!(generic.solution, Solution::Unique { .. }), + "{generic:?}" + ); + assert_eq!(generic.stats.generator_steps, 1, "{generic:?}"); + + let given = solve_class_report( + &db, + env, + class_id(&db, module, "GivenTarget"), + word, + Vec::new(), + ); + assert!( + matches!(given.solution, Solution::Unique { .. }), + "{given:?}" + ); + assert_eq!(given.stats.generator_steps, 1, "{given:?}"); + + let superclass = + solve_class_report(&db, env, class_id(&db, module, "Parent"), word, Vec::new()); + assert!( + matches!(superclass.solution, Solution::Unique { .. }), + "{superclass:?}" + ); + assert_eq!(superclass.stats.generator_steps, 2, "{superclass:?}"); + + let default = solve_class_report( + &db, + env, + class_id(&db, module, "DefaultTarget"), + Ty::string(&db), + Vec::new(), + ); + assert!( + matches!(default.solution, Solution::Unique { .. }), + "{default:?}" + ); + assert_eq!(default.stats.generator_steps, 1, "{default:?}"); + + let ambiguous = solve_class_report( + &db, + env, + class_id(&db, module, "AmbiguousTarget"), + word, + Vec::new(), + ); + assert!( + matches!(ambiguous.solution, Solution::Ambiguous { .. }), + "{ambiguous:?}" + ); + assert_eq!(ambiguous.stats.generator_steps, 2, "{ambiguous:?}"); +} + +#[test] +fn trait_solver_preserves_comptime_transparent_fixed_local_given() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Typedef {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let base_env = trait_env(&db, module, &module_resolution); + let class = class_id(&db, module, "Typedef"); + let context_ty = Ty::bound(&db, 0); + let env = trait_env_with_givens( + &db, + base_env, + vec![Pred::in_class(&db, class, context_ty, vec![Ty::word(&db)])], + ); + let goal = Pred::in_class( + &db, + class, + Ty::comptime(&db, context_ty), + vec![Ty::word(&db)], + ); + + let report = solve_report(&db, env, canonical_goal(&db, goal)); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert_eq!(report.stats.generator_steps, 1, "{report:?}"); +} + +#[test] +fn trait_solver_preserves_rigid_origin_across_nested_goal_canonicalization() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Wrap { Wrap(a) } + +trait Foo {} +trait Bar {} + +impl Bar, rep> where a: Foo {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let base_env = trait_env(&db, module, &module_resolution); + let foo = class_id(&db, module, "Foo"); + let bar = class_id(&db, module, "Bar"); + let rigid = Ty::bound(&db, 0); + let result = Ty::bound(&db, 1); + let env = trait_env_with_givens( + &db, + base_env, + vec![Pred::in_class(&db, foo, rigid, vec![Ty::word(&db)])], + ); + let goal = Pred::in_class( + &db, + bar, + adt_ty(&db, module, "Wrap", vec![rigid]), + vec![result], + ); + + let report = solve_report( + &db, + env, + crate::canonical_goal_with_allowed(&db, goal, vec![1]), + ); + + let Solution::Unique { + subst, + evidence: Evidence::Instance { sub_evidence, .. }, + } = &report.solution + else { + panic!("expected improved nested solution, got {report:?}"); + }; + assert_eq!(subst.values, vec![(1, Ty::word(&db))]); + assert!(matches!( + sub_evidence.as_slice(), + [Evidence::Builtin { pred }] + if matches!( + pred.kind(&db), + PredKind::InClass { class, main, args } + if *class == foo && *main == rigid && args == &vec![Ty::word(&db)] + ) + )); + assert!(!report.exhausted, "{report:?}"); +} + +#[test] +fn inference_improves_multi_parameter_result_through_local_given() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Wrap { Wrap(a) } + +trait Foo {} +trait Bar {} + +impl Bar, rep> where a: Foo {} + +function need_bar(x: Wrap) returns () where Wrap: Bar { + return (); +} + +function use_bar(x: Wrap) returns () where a: Foo { + need_bar(x); + return (); +} +"#, + ); + let result = infer_all_functions_with_solver(&db, module) + .into_iter() + .find_map(|(name, result)| (name == "use_bar").then_some(result)) + .expect("use_bar inference result"); + + assert_no_typeck(&result); + assert!( + result.call_site_evidence.iter().any(|evidence| { + matches!( + evidence.callee, + CallSiteCallee::Function(def) + if def.name(&db).as_deref() == Some("need_bar") + ) + }), + "expected solved need_bar call evidence, got {:?}", + result.call_site_evidence + ); +} + +#[test] +fn trait_solver_prefilter_preserves_comptime_correlated_instance_head() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Correlated {} +impl Correlated<(comptime x, x)> {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let context_ty = Ty::bound(&db, 0); + let pair = Ty::named( + &db, + TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + vec![context_ty, context_ty], + ); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Correlated"), + pair, + Vec::new(), + ); + + assert!( + matches!(report.solution, Solution::Unique { .. }), + "{report:?}" + ); + assert_eq!(report.stats.generator_steps, 1, "{report:?}"); +} + +#[test] +fn trait_solver_prefers_specific_instance_over_default() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Test {} +default impl Test {} +impl Test {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let class = class_id(&db, module, "Test"); + let specific = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::InstanceDef(instance) if instance.default_kw(&db).is_none() => { + Some(instance.def_id_value(&db)) + } + _ => None, + }) + .next() + .expect("specific impl"); + + let solution = solve_class_goal(&db, env, class, Ty::word(&db), Vec::new()); + let Solution::Unique { evidence, .. } = solution else { + panic!("expected unique solution, got {solution:?}"); + }; + assert!(matches!( + evidence, + Evidence::Instance { instance, .. } if instance == specific + )); + + let default_solution = solve_class_goal(&db, env, class, Ty::string(&db), Vec::new()); + assert!(matches!(default_solution, Solution::Unique { .. })); +} + +#[test] +fn trait_solver_uses_default_instance_for_non_default_clause_condition() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Wrap { Wrap(a) } + +trait DefaultDependency {} +default impl DefaultDependency {} + +trait Outer {} +impl Outer> where a: DefaultDependency {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let wrapped_word = adt_ty(&db, module, "Wrap", vec![Ty::word(&db)]); + let default_dependency = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::InstanceDef(instance) if instance.default_kw(&db).is_some() => { + Some(instance.def_id_value(&db)) + } + _ => None, + }) + .expect("default dependency impl"); + + let report = solve_class_report( + &db, + env, + class_id(&db, module, "Outer"), + wrapped_word, + Vec::new(), + ); + + let Solution::Unique { ref evidence, .. } = report.solution else { + panic!("expected default-backed solution, got {report:?}"); + }; + let Evidence::Instance { sub_evidence, .. } = evidence else { + panic!("expected outer impl evidence"); + }; + assert_eq!(sub_evidence.len(), 1); + assert!(matches!( + &sub_evidence[0], + Evidence::Instance { instance, .. } if *instance == default_dependency + )); + assert!(!report.exhausted, "{report:?}"); + assert_eq!(report.stats.generator_steps, 2, "{report:?}"); +} + +#[test] +fn trait_solver_reports_overlapping_non_default_instances_as_ambiguous() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait C {} +impl C {} +impl C {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "C"), + Ty::word(&db), + Vec::new(), + ); + assert!(matches!( + solution, + Solution::Ambiguous { candidates } if candidates.len() == 2 + )); +} + +#[test] +fn trait_solver_keeps_distinct_substitutions_from_the_same_instance() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Pair { Pair(a, b) } + +trait D {} +default impl D {} +default impl D {} + +trait C {} +impl C> where a: D {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let goal = adt_ty( + &db, + module, + "Pair", + vec![Ty::string(&db), Ty::bound(&db, 0)], + ); + + let goal = Pred::in_class(&db, class_id(&db, module, "C"), goal, Vec::new()); + let solution = solve( + &db, + env, + crate::canonical_goal_with_allowed(&db, goal, vec![0]), + ); + + let Solution::Ambiguous { candidates } = solution else { + panic!("expected ambiguous same-impl substitutions, got {solution:?}"); + }; + assert_eq!(candidates.len(), 2); + let substitutions = candidates + .iter() + .map(|candidate| candidate.subst.values.clone()) + .collect::>(); + assert_eq!(substitutions.len(), 2); +} + +#[test] +fn trait_solver_unifies_weak_class_args_across_conditions() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Uint { Uint(word) } + +trait Typedef {} +impl Typedef {} + +trait StorageSize {} +impl StorageSize {} + +impl StorageSize where a: Typedef, b: StorageSize {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let uint = adt_ty(&db, module, "Uint", Vec::new()); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "StorageSize"), + uint, + Vec::new(), + ); + + let Solution::Unique { evidence, .. } = solution else { + panic!("expected weak trait argument unification, got {solution:?}"); + }; + let Evidence::Instance { args, .. } = evidence else { + panic!("expected generic StorageSize impl evidence"); + }; + assert_eq!(args, vec![uint, Ty::word(&db)]); +} + +#[test] +fn default_instance_is_blocked_by_unifying_normal_head() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait C {} +impl C {} +default impl C {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "C"), + Ty::bound(&db, 0), + Vec::new(), + ); + + assert!(matches!(solution, Solution::NoSolution)); +} + +#[test] +fn imported_class_origin_contributes_superclass_clauses() { + let mut db = TestDb::default(); + let lib_path = PathBuf::from("/main/lib.solc"); + let main_path = PathBuf::from("/main/main.solc"); + let lib_file = source_file_at_path( + &db, + &lib_path, + r#" +export { Eq, Ord }; + +trait Eq {} +trait Ord where a: Eq {} +"#, + ); + let main_file = source_file_at_path( + &db, + &main_path, + r#" +import {Eq, Ord} from lib; + +impl Ord {} +"#, + ); + let lib_key = module_key_for_path(LibraryId::Main, &PathBuf::from("/main"), &lib_path).unwrap(); + let main_key = + module_key_for_path(LibraryId::Main, &PathBuf::from("/main"), &main_path).unwrap(); + db.insert_module_file(lib_key.clone(), lib_file); + db.insert_module_file(main_key.clone(), main_file); + let lib_module = module_id_from_key(&db, &lib_key); + let main_module = module_id_from_key(&db, &main_key); + let lib_hir = parse_file_to_hir(&db, lib_file).module(&db); + + let env = trait_env_for_module(&db, main_module); + let solution = solve_class_goal( + &db, + env, + class_id(&db, lib_hir, "Eq"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!( + solution, + Solution::Unique { + evidence: Evidence::Superclass { .. }, + .. + } + )); + assert_eq!(lib_module.display(&db), "lib"); +} + +#[test] +fn trait_env_from_module_resolution_and_imports_deduplicates_superclass_modules() { + let mut db = TestDb::default(); + let lib_path = PathBuf::from("/main/lib.solc"); + let main_path = PathBuf::from("/main/main.solc"); + let lib_file = source_file_at_path( + &db, + &lib_path, + r#" +export { Parent, Child }; + +trait Parent {} +trait Child where a: Parent {} +"#, + ); + let main_file = source_file_at_path( + &db, + &main_path, + r#" +import {Parent, Child} from lib; +"#, + ); + let lib_key = module_key_for_path(LibraryId::Main, &PathBuf::from("/main"), &lib_path).unwrap(); + let main_key = + module_key_for_path(LibraryId::Main, &PathBuf::from("/main"), &main_path).unwrap(); + db.insert_module_file(lib_key, lib_file); + db.insert_module_file(main_key.clone(), main_file); + + let main_module = module_id_from_key(&db, &main_key); + let main_hir = parse_file_to_hir(&db, main_file).module(&db); + let imports = nameres::module_env_for_hir_module(&db, main_module, main_hir); + let item_scope = imports.item_scope.clone().expect("main item scope"); + let resolution = hir_nameres::resolve_module_with_imports(&db, main_hir, item_scope, &imports); + let trait_env = + trait_env_from_module_resolution_and_imports(&db, main_hir, &resolution, &imports); + let ClassId::User(child_def) = + class_id(&db, parse_file_to_hir(&db, lib_file).module(&db), "Child") + else { + panic!("Child must be a user-defined trait"); + }; + let superclass_origins = trait_env + .clauses(&db) + .iter() + .filter_map(|clause| match &clause.origin { + ClauseOrigin::Superclass(def) => Some(*def), + _ => None, + }) + .collect::>(); + + assert_eq!(superclass_origins, vec![child_def]); +} + +#[test] +fn superclass_solution_records_projection_evidence() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Eq {} +trait Ord where a: Eq {} +impl Ord {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "Eq"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!( + solution, + Solution::Unique { + evidence: Evidence::Superclass { + child, + .. + }, + .. + } if matches!(*child, Evidence::Instance { .. }) + )); +} + +#[test] +fn direct_instance_precedes_superclass_projection() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Eq {} +trait Ord where a: Eq {} +impl Eq {} +impl Ord {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "Eq"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!( + solution, + Solution::Unique { + evidence: Evidence::Instance { .. }, + .. + } + )); +} + +#[test] +fn local_givens_and_superclasses_precede_global_instances() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +trait Eq {} +trait Ord where a: Eq {} +impl Eq {} +"#, + ); + let module_resolution = hir_nameres::resolve_module(&db, module); + let env = trait_env(&db, module, &module_resolution); + let env = trait_env_with_givens( + &db, + env, + vec![Pred::in_class( + &db, + class_id(&db, module, "Ord"), + Ty::word(&db), + Vec::new(), + )], + ); + + let solution = solve_class_goal( + &db, + env, + class_id(&db, module, "Eq"), + Ty::word(&db), + Vec::new(), + ); + + assert!(matches!( + solution, + Solution::Unique { + evidence: Evidence::Superclass { + child, + .. + }, + .. + } if matches!(*child, Evidence::Builtin { .. }) + )); +} + +#[test] +fn pragma_corpus_files_have_no_instance_soundness_diagnostics() { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let corpus = manifest.join("../parser/tests/fixtures/corpus"); + let files = [ + "pragmas/coverage.solc", + "cases/array.solc", + "cases/bound-with-pragma.solc", + "cases/tabled-left-recursive-fail.solc", + "cases/tabled-cycle-fail.solc", + "cases/mptc-partial-instance.solc", + ]; + + for file in files { + let path = ["ok", "fail"] + .into_iter() + .map(|status| corpus.join(status).join("test/examples").join(file)) + .find(|path| path.exists()) + .expect("corpus fixture"); + let src = std::fs::read_to_string(path).expect("fixture source"); + let (db, key) = db_with_main_typeck(&src); + let source = *db.module_files.get(&key).expect("main source"); + assert!( + parser::parse_diagnostics(&db, source).is_empty(), + "{file} should parse cleanly" + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = crate::solver::instance_soundness_diagnostics(&db, module_id).clone(); + assert!( + diagnostics.is_empty(), + "{file} produced impl soundness diagnostics: {diagnostics:?}" + ); + } +} + +#[test] +fn structured_default_instance_head_is_allowed_only_when_it_contains_a_type_variable() { + let (db, key) = db_with_main_typeck( + r#" +enum Box { Box(a) } +trait Marker {} +default impl Marker> {} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = crate::solver::instance_soundness_diagnostics(&db, module_id); + assert!( + diagnostics.iter().all(|diagnostic| !matches!( + diagnostic, + TypeckDiagnostic::InvalidDefaultInstance { .. } + )), + "{diagnostics:?}" + ); + + let (db, key) = db_with_main_typeck( + r#" +enum Box { Box(a) } +trait Marker {} +default impl Marker> {} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = crate::solver::instance_soundness_diagnostics(&db, module_id); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + TypeckDiagnostic::InvalidDefaultInstance { .. } + )), + "{diagnostics:?}" + ); +} diff --git a/crates/hir-ty/src/infer/unify.rs b/crates/hir-ty/src/infer/unify.rs new file mode 100644 index 00000000..d1f8502e --- /dev/null +++ b/crates/hir-ty/src/infer/unify.rs @@ -0,0 +1,222 @@ +use super::*; + +impl<'db> InferCtx<'db> { + pub(super) fn unify_at( + &mut self, + span: LabelSpan, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> bool { + self.unify_at_with_expected_display(span, expected, actual, None) + } + + fn unify_at_with_expected_display( + &mut self, + span: LabelSpan, + expected: InferTy<'db>, + actual: InferTy<'db>, + expected_display: Option, + ) -> bool { + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + let expected = self.normalize_aliases(expected); + let actual = self.normalize_aliases(actual); + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + if let Err(err) = self.engine.unify(expected, actual) { + let diagnostic = match err { + UnifyError::Mismatch { expected, actual } => { + let expected = expected_display.unwrap_or_else(|| { + self.engine + .display_with_names(expected, &self.type_var_names) + }); + let actual = self.engine.display_with_names(actual, &self.type_var_names); + TypeckDiagnostic::Mismatch { + span, + expected, + actual, + } + } + err => err.diagnostic(&mut self.engine, span, &self.type_var_names), + }; + self.diagnostics.push(diagnostic); + false + } else { + true + } + } + + pub(super) fn unify_span( + &mut self, + span: Span<'db>, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) { + self.unify_at(self.label_span(span), expected, actual); + } + + pub(super) fn unify_body( + &mut self, + body: FuncBody<'db>, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) { + self.unify_at(self.body_label_span(body), expected, actual); + } + + pub(super) fn unify_stmt( + &mut self, + body: FuncBody<'db>, + stmt: Id>, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> bool { + self.unify_at(self.stmt_label_span(body, stmt), expected, actual) + } + + pub(super) fn unify_expr( + &mut self, + body: FuncBody<'db>, + expr: Id>, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> bool { + let expected_display = self.expected_expr_displays.get(&(body, expr)).cloned(); + let ok = self.unify_at_with_expected_display( + self.expr_label_span(body, expr), + expected, + actual, + expected_display, + ); + if !ok { + self.poison_expr(body, expr); + } + ok + } + + pub(super) fn unify_call_arg( + &mut self, + body: FuncBody<'db>, + expr: Id>, + expected: InferTy<'db>, + actual: InferTy<'db>, + context: CallArgDiagnostic, + ) -> bool { + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + let expected = self.normalize_aliases(expected); + let actual = self.normalize_aliases(actual); + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + let span = self.expr_label_span(body, expr); + let ok = match self.engine.unify(expected, actual) { + Ok(()) => true, + Err(UnifyError::Mismatch { expected, actual }) => { + let expected = context + .param + .ty + .clone() + .unwrap_or_else(|| self.display_infer_ty(expected)); + let actual = self.display_infer_ty(actual); + self.diagnostics.push(TypeckDiagnostic::ArgMismatch { + span, + expected, + actual, + callee: context.callee, + param: context.param, + }); + false + } + Err(err) => { + self.diagnostics + .push(err.diagnostic(&mut self.engine, span, &self.type_var_names)); + false + } + }; + if !ok { + self.poison_expr(body, expr); + } + ok + } + + pub(super) fn unify_pat( + &mut self, + body: FuncBody<'db>, + pat: Id>, + expected: InferTy<'db>, + actual: InferTy<'db>, + ) -> bool { + let ok = self.unify_at(self.pat_label_span(body, pat), expected, actual); + if !ok { + self.poison_pat(body, pat); + } + ok + } + + pub(super) fn unify(&mut self, expected: InferTy<'db>, actual: InferTy<'db>) { + self.unify_at(self.label_span(self.module.span(self.db)), expected, actual); + } + + pub(super) fn can_unify(&mut self, expected: InferTy<'db>, actual: InferTy<'db>) -> bool { + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + let expected = self.normalize_aliases(expected); + let actual = self.normalize_aliases(actual); + if matches!(expected, InferTy::Error) || matches!(actual, InferTy::Error) { + return true; + } + self.engine.can_unify(expected, actual) + } + + pub(super) fn normalize_aliases(&mut self, ty: InferTy<'db>) -> InferTy<'db> { + if !infer_ty_mentions_alias(&ty) { + return ty; + } + let item_resolutions = self.item_resolutions_for_aliases(); + let mut normalizer = AliasNormalizer::new(self.db, self.module, &item_resolutions); + let value = normalizer.normalize_ty(ty); + self.diagnostics.extend( + normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic), + ); + value + } + + pub(super) fn normalize_pred_aliases(&mut self, pred: Pred<'db>) -> Pred<'db> { + if !pred_mentions_alias(self.db, pred) { + return pred; + } + let item_resolutions = self.item_resolutions_for_aliases(); + let mut normalizer = AliasNormalizer::new(self.db, self.module, &item_resolutions); + let value = normalizer.normalize_pred(pred); + self.diagnostics.extend( + normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic), + ); + value + } + + pub(super) fn item_resolutions_for_aliases(&self) -> hir_nameres::ItemResolutionFacts<'db> { + if let Some(entry_module) = self.entry_module { + let env = nameres::module_import_surface(self.db, entry_module); + if let Some(scope) = env.item_scope.as_ref() { + return hir_nameres::resolve_item_type_facts_with_imports( + self.db, + self.module, + scope, + &env, + ); + } + } + hir_nameres::resolve_item_type_facts(self.db, self.module) + } +} diff --git a/crates/hir-ty/src/infer/yul.rs b/crates/hir-ty/src/infer/yul.rs new file mode 100644 index 00000000..17c86753 --- /dev/null +++ b/crates/hir-ty/src/infer/yul.rs @@ -0,0 +1,446 @@ +use super::*; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct YulFunctionSig<'db> { + params: Vec>, + ret: InferTy<'db>, +} + +#[derive(Debug, Clone, Default)] +struct YulScope<'db> { + values: FxHashSet, + functions: FxHashMap>, +} + +impl<'db> InferCtx<'db> { + pub(super) fn infer_yul_block(&mut self, body: &[YulStmt<'db>]) -> (Vec, InferTy<'db>) { + let mut scopes = vec![YulScope::default()]; + self.infer_yul_block_scoped(body, &mut scopes) + } + + fn infer_yul_block_scoped( + &mut self, + body: &[YulStmt<'db>], + scopes: &mut Vec>, + ) -> (Vec, InferTy<'db>) { + let mut binds = Vec::new(); + let mut ty = self.unit(); + for stmt in body { + let (new_binds, stmt_ty) = self.infer_yul_stmt(stmt, scopes); + binds.extend(new_binds); + ty = stmt_ty; + } + (binds, ty) + } + + fn infer_yul_stmt( + &mut self, + stmt: &YulStmt<'db>, + scopes: &mut Vec>, + ) -> (Vec, InferTy<'db>) { + match &stmt.kind { + YulStmtKind::Block(body) => { + scopes.push(YulScope::default()); + self.infer_yul_block_scoped(body, scopes); + scopes.pop(); + (Vec::new(), self.unit()) + } + YulStmtKind::Let { names, init } => { + if let Some(init) = init { + let init_ty = self.infer_yul_expr(init, scopes); + self.check_yul_assign_arity( + self.yul_stmt_label_span(stmt), + "Yul let", + names.len(), + init_ty, + ); + } + let binds = names + .iter() + .map(|name| (*name.atom()).text(self.db).to_owned()) + .collect::>(); + for name in &binds { + self.add_yul_local(scopes, name); + } + (binds, self.unit()) + } + YulStmtKind::Assign { names, value } => { + let value_ty = self.infer_yul_expr(value, scopes); + self.check_yul_assign_arity( + self.yul_stmt_label_span(stmt), + "Yul assignment", + names.len(), + value_ty, + ); + for name in names { + let text = (*name.atom()).text(self.db); + if !self.is_yul_local(scopes, text) { + self.check_yul_sail_var_write(self.label_span(name.span(self.db)), text); + } + } + (Vec::new(), self.unit()) + } + YulStmtKind::Expr(expr) => (Vec::new(), self.infer_yul_expr(expr, scopes)), + YulStmtKind::If { cond, body } => { + self.infer_yul_expr(cond, scopes); + scopes.push(YulScope::default()); + self.infer_yul_block_scoped(body, scopes); + scopes.pop(); + (Vec::new(), self.unit()) + } + YulStmtKind::For { + init, + cond, + post, + body, + } => { + scopes.push(YulScope::default()); + self.infer_yul_block_scoped(init, scopes); + self.infer_yul_expr(cond, scopes); + self.infer_yul_block_scoped(body, scopes); + self.infer_yul_block_scoped(post, scopes); + scopes.pop(); + (Vec::new(), self.unit()) + } + YulStmtKind::Switch { + expr, + cases, + default, + } => { + self.infer_yul_expr(expr, scopes); + for case in cases { + self.infer_yul_case(case, scopes); + } + if let Some(default) = default { + scopes.push(YulScope::default()); + self.infer_yul_block_scoped(default, scopes); + scopes.pop(); + } + (Vec::new(), self.unit()) + } + YulStmtKind::FunctionDef { + name, + params, + rets, + body, + } => { + let fn_name = (*name.atom()).text(self.db).to_owned(); + let sig = YulFunctionSig { + params: self.yul_word_tys(params.len()), + ret: self.yul_return_ty(rets.len()), + }; + self.add_yul_function(scopes, fn_name, sig); + scopes.push(YulScope::default()); + for name in params.iter().chain(rets) { + self.add_yul_local(scopes, (*name.atom()).text(self.db)); + } + self.infer_yul_block_scoped(body, scopes); + scopes.pop(); + (Vec::new(), self.unit()) + } + YulStmtKind::Leave | YulStmtKind::Break | YulStmtKind::Continue => { + (Vec::new(), self.unit()) + } + YulStmtKind::Error => (Vec::new(), InferTy::Error), + } + } + + fn infer_yul_case(&mut self, case: &YulCase<'db>, scopes: &mut Vec>) { + self.infer_yul_lit(&case.lit); + scopes.push(YulScope::default()); + self.infer_yul_block_scoped(&case.body, scopes); + scopes.pop(); + } + + fn infer_yul_expr( + &mut self, + expr: &YulExpr<'db>, + scopes: &mut Vec>, + ) -> InferTy<'db> { + match &expr.kind { + YulExprKind::Lit(lit) => self.infer_yul_lit(lit), + YulExprKind::Ident(name) => { + let text = (*name.atom()).text(self.db); + if self.is_yul_local(scopes, text) { + self.word() + } else { + self.check_yul_sail_var_read(self.yul_expr_label_span(expr), text) + } + } + YulExprKind::Call { name, args } => { + let text = (*name.atom()).text(self.db); + let arg_tys = args + .iter() + .map(|arg| self.infer_yul_expr(arg, scopes)) + .collect::>(); + let sig = self + .lookup_yul_function(scopes, text) + .or_else(|| self.yul_builtin_sig(text)); + let Some(sig) = sig else { + self.diagnostics.push(TypeckDiagnostic::UnknownYulName { + span: self.yul_expr_label_span(expr), + name: text.to_owned(), + }); + return InferTy::Error; + }; + if sig.params.len() != arg_tys.len() { + self.diagnostics.push(TypeckDiagnostic::WrongArity { + span: self.yul_expr_label_span(expr), + context: format!("Yul call `{text}`"), + expected: sig.params.len(), + actual: arg_tys.len(), + callee: None, + }); + } + for ((expected, actual), arg) in sig.params.iter().cloned().zip(arg_tys).zip(args) { + self.unify_at(self.yul_expr_label_span(arg), expected, actual); + } + sig.ret + } + YulExprKind::Error => InferTy::Error, + } + } + + fn infer_yul_lit(&mut self, lit: &YulLitKind) -> InferTy<'db> { + match lit { + YulLitKind::Number(_) | YulLitKind::Hex(_) | YulLitKind::Bool(_) => self.word(), + YulLitKind::String(_) => self.string(), + YulLitKind::Error => InferTy::Error, + } + } + + fn add_yul_local(&self, scopes: &mut [YulScope<'db>], name: &str) { + if let Some(scope) = scopes.last_mut() { + scope.values.insert(name.to_owned()); + } + } + + fn add_yul_function( + &self, + scopes: &mut [YulScope<'db>], + name: String, + sig: YulFunctionSig<'db>, + ) { + if let Some(scope) = scopes.last_mut() { + scope.functions.insert(name, sig); + } + } + + fn is_yul_local(&self, scopes: &[YulScope<'db>], name: &str) -> bool { + scopes.iter().rev().any(|scope| scope.values.contains(name)) + } + + fn lookup_yul_function( + &self, + scopes: &[YulScope<'db>], + name: &str, + ) -> Option> { + scopes + .iter() + .rev() + .find_map(|scope| scope.functions.get(name).cloned()) + } + + fn check_yul_sail_var_read(&mut self, span: LabelSpan, name: &str) -> InferTy<'db> { + let Some(ty) = self.lookup_sail_local(name) else { + self.diagnostics.push(TypeckDiagnostic::UnknownYulName { + span, + name: name.to_owned(), + }); + return InferTy::Error; + }; + let word = self.word(); + if self.can_unify(ty.clone(), word.clone()) { + self.unify_at(span, ty, word.clone()); + } else { + let actual = self.display_infer_ty(ty); + self.diagnostics.push(TypeckDiagnostic::NonWordYulVar { + span, + name: name.to_owned(), + actual, + }); + } + word + } + + fn check_yul_sail_var_write(&mut self, span: LabelSpan, name: &str) { + let Some(ty) = self.lookup_sail_local(name) else { + return; + }; + let word = self.word(); + if self.can_unify(ty.clone(), word.clone()) { + self.unify_at(span, ty, word); + } else { + let actual = self.display_infer_ty(ty); + self.diagnostics.push(TypeckDiagnostic::NonWordYulVar { + span, + name: name.to_owned(), + actual, + }); + } + } + + fn check_yul_assign_arity( + &mut self, + span: LabelSpan, + context: &str, + expected: usize, + actual_ty: InferTy<'db>, + ) { + if matches!(self.engine.resolve(actual_ty.clone()), InferTy::Error) { + return; + } + let actual = self.yul_return_arity(actual_ty); + if expected != actual { + self.diagnostics.push(TypeckDiagnostic::WrongArity { + span, + context: context.to_owned(), + expected, + actual, + callee: None, + }); + } + } + + fn yul_return_arity(&mut self, ty: InferTy<'db>) -> usize { + let ty = self.normalize_aliases(ty); + match self.engine.resolve(ty) { + InferTy::Error => 0, + InferTy::Tuple(elems) => elems.len(), + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + } if args.is_empty() => 0, + InferTy::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => 1 + self.yul_return_arity(args[1].clone()), + _ => 1, + } + } + + fn yul_word_tys(&mut self, count: usize) -> Vec> { + let word = self.word(); + vec![word; count] + } + + fn yul_return_ty(&mut self, count: usize) -> InferTy<'db> { + match count { + 0 => self.unit(), + 1 => self.word(), + _ => InferTy::Tuple(self.yul_word_tys(count)), + } + } + + fn yul_builtin_sig(&mut self, name: &str) -> Option> { + let word = self.word(); + let string = self.string(); + let unit = self.unit(); + let word_params = |count: usize| vec![word.clone(); count]; + let sig = match name { + "stop" | "invalid" => YulFunctionSig { + params: Vec::new(), + ret: unit.clone(), + }, + "add" | "mul" | "sub" | "div" | "sdiv" | "mod" | "smod" | "exp" | "signextend" + | "lt" | "gt" | "slt" | "sgt" | "eq" | "and" | "or" | "xor" | "byte" | "shl" + | "shr" | "sar" | "keccak256" => YulFunctionSig { + params: word_params(2), + ret: word.clone(), + }, + "addmod" | "mulmod" => YulFunctionSig { + params: word_params(3), + ret: word.clone(), + }, + "iszero" | "not" | "clz" | "balance" | "calldataload" | "extcodesize" + | "extcodehash" | "blockhash" | "blobhash" | "pop" | "mload" | "sload" | "tload" + | "selfdestruct" => { + let ret = if matches!(name, "pop" | "selfdestruct") { + unit.clone() + } else { + word.clone() + }; + YulFunctionSig { + params: word_params(1), + ret, + } + } + "address" | "origin" | "caller" | "callvalue" | "calldatasize" | "codesize" + | "gasprice" | "returndatasize" | "coinbase" | "timestamp" | "number" + | "prevrandao" | "gaslimit" | "chainid" | "selfbalance" | "basefee" | "blobbasefee" + | "msize" | "gas" => YulFunctionSig { + params: Vec::new(), + ret: word.clone(), + }, + "calldatacopy" | "codecopy" | "returndatacopy" | "mstore" | "mstore8" | "sstore" + | "tstore" | "mcopy" | "datacopy" => YulFunctionSig { + params: word_params(3) + .into_iter() + .take(match name { + "mstore" | "mstore8" | "sstore" | "tstore" => 2, + _ => 3, + }) + .collect(), + ret: unit.clone(), + }, + "extcodecopy" => YulFunctionSig { + params: word_params(4), + ret: unit.clone(), + }, + "log0" => YulFunctionSig { + params: word_params(2), + ret: unit.clone(), + }, + "log1" => YulFunctionSig { + params: word_params(3), + ret: unit.clone(), + }, + "log2" => YulFunctionSig { + params: word_params(4), + ret: unit.clone(), + }, + "log3" => YulFunctionSig { + params: word_params(5), + ret: unit.clone(), + }, + "log4" => YulFunctionSig { + params: word_params(6), + ret: unit.clone(), + }, + "create" => YulFunctionSig { + params: word_params(3), + ret: word.clone(), + }, + "create2" => YulFunctionSig { + params: word_params(4), + ret: word.clone(), + }, + "call" | "callcode" => YulFunctionSig { + params: word_params(7), + ret: word.clone(), + }, + "delegatecall" | "staticcall" => YulFunctionSig { + params: word_params(6), + ret: word.clone(), + }, + "return" | "revert" => YulFunctionSig { + params: word_params(2), + ret: self.engine.fresh_var(), + }, + "datasize" | "dataoffset" | "loadimmutable" | "linkersymbol" => YulFunctionSig { + params: vec![string.clone()], + ret: word.clone(), + }, + "setimmutable" => YulFunctionSig { + params: vec![word.clone(), string.clone(), word.clone()], + ret: unit.clone(), + }, + "memoryguard" => YulFunctionSig { + params: word_params(1), + ret: word.clone(), + }, + _ => return None, + }; + Some(sig) + } +} diff --git a/crates/hir-ty/src/lib.rs b/crates/hir-ty/src/lib.rs new file mode 100644 index 00000000..760c28dd --- /dev/null +++ b/crates/hir-ty/src/lib.rs @@ -0,0 +1,123 @@ +//! Type lowering and inference for HIR. +//! +//! `solcore-hir-ty` sits above HIR and name resolution. It keeps the interned +//! ground semantic type model free of inference variables, and uses ephemeral +//! ena-backed inference state only inside query execution. + +pub mod alias; +pub mod contract; +mod coverage; +pub mod desugar; +mod display; +pub mod infer; +pub mod lower; +pub mod prepare; +pub mod solver; +mod support; +mod value_type; + +pub use alias::{ + AliasError, AliasNorm, AliasNormalizer, AliasType, AliasTypeKind, normalize_pred_aliases, + normalize_scheme_aliases, normalize_ty_aliases, type_alias_normalization_errors, +}; +pub use contract::{ + AbiParam, AbiSelector, AbiSignature, AbiType, BodyDesugarPlan, BoolNode, DispatchConstructor, + DispatchFallback, DispatchMethod, DispatchSurface, FrontendDesugarPlan, FrontendTransform, + IndirectArgShape, abi_selector, contract_abi_json, contract_dispatch_surface, + contract_dispatch_surface_for_module, contract_needs_generated_dispatch, frontend_desugar_plan, + module_contract_diagnostics, +}; +pub use desugar::{ + BodyDesugarView, BodyPreTypeckDesugarPlan, BoolUnitSumNode, BoolUnitSumView, + FieldInitPreTypeckDesugarPlan, FieldInitPreTypeckTransform, PreTypeckDesugarPlan, + PreTypeckTransform, ProductShape, SourceOrigin, SourceOriginKind, TypeProductDesugar, + pre_typeck_desugar_body_tree, pre_typeck_desugar_plan, +}; +pub use display::display_type_ref_source; +pub use hir::sema::ty::{ + BoundTyVar, BuiltinClassId, BuiltinTyCtor, ClassId, Pred, PredKind, QualTy, Ty, TyCtor, TyKind, + TyScheme, UserTyCtor, UserTyCtorKind, +}; +pub use infer::{ + AdtCtorScheme, AdtFieldSelection, BodyTyContext, CallSiteCallee, CallSiteEvidence, + CalleeDiagnostic, CheckedConversion, ComptimeObligationKind, ConversionKind, + DeferredObligation, ExprTy, InferResultExt, InferTable, InferTy, InferenceResult, Instantiated, + LetTy, ObligationEvidence, ObligationSource, ParameterDiagnostic, PatTy, TyVid, + TypeckDiagnostic, UnifyError, VarValue, body_ty_diagnostics, function_scheme, infer_body, + lower_normalized_function_with_inferred_signature, +}; +pub use lower::{ + BinderEnv, LoweredAdtCtor, LoweredField, LoweredFunction, LoweredTypeAlias, TypeLowering, + TypeLoweringDiagnostic, builtin_scheme, class_method_type_vars, +}; +pub use prepare::{ + GeneratedOrigin, GeneratedOriginKind, GeneratedOriginMap, PreparedModule, + contract_overlay_backend_name, is_contract_deployment_main_def, is_contract_dispatch_main_def, + prepare_module, +}; +pub use solver::{ + BaseTraitEnvId, BaseTraitEnvSource, Candidate, CanonicalGoal, ClauseOrigin, + DerivedGenericClauseSource, DerivedGenericFromArm, DerivedGenericPlan, DerivedGenericToArm, + Evidence, LocalGivensId, ModuleTraitEnvSource, ProgramClause, Solution, SolverReport, + Substitution, TraitClauseSetId, TraitEnvId, canonical_goal, canonical_goal_with_allowed, + derived_generic_instance_plan, derived_generic_plan, instance_soundness_diagnostics, solve, + solve_report, trait_env_for_module, trait_env_from_module_resolution, + trait_env_from_module_resolution_and_imports, trait_env_with_givens, +}; +pub use value_type::{ + ValueTypeError, value_type_underlying, value_type_underlying_has_word_storage_representation, + value_type_underlying_in_context, +}; + +/// Database contract required by HIR type queries. +#[salsa::db] +pub trait Db: nameres::Db {} + +/// Collects lowered frontend diagnostics reachable from `entry`. +/// +/// Full module resolution is forced before name-resolution and type-checking +/// diagnostics are collected. The result is deterministically sorted and +/// deduplicated for publication by drivers and analysis hosts. +#[tracing::instrument( + target = "hir_ty::frontend", + level = "debug", + skip_all, + fields(entry = %entry.display(db)) +)] +pub fn collect_frontend_diagnostics<'db>( + db: &'db dyn Db, + entry: nameres::ModuleId<'db>, +) -> Vec { + let graph = nameres::resolve_reachable_full(db, entry); + let mut diagnostics = nameres::reachable_diagnostics(db, entry) + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect::>(); + let nameres_diagnostics = diagnostics.len(); + diagnostics.extend( + infer::reachable_typeck_diagnostics(db, entry) + .iter() + .map(|diagnostic| diagnostic.lower(db)), + ); + let typeck_diagnostics = diagnostics.len() - nameres_diagnostics; + hir::diag::sort_dedup_rendered_diagnostics(db, &mut diagnostics); + let errors = diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == hir::diag::DiagnosticLevel::Error) + .count(); + let warnings = diagnostics + .iter() + .filter(|diagnostic| diagnostic.level == hir::diag::DiagnosticLevel::Warning) + .count(); + tracing::debug!( + target: "hir_ty::frontend", + modules = graph.modules.len(), + nameres_diagnostics, + typeck_diagnostics, + diagnostics = diagnostics.len(), + errors, + warnings, + "frontend diagnostics collected" + ); + diagnostics +} diff --git a/crates/hir-ty/src/lower.rs b/crates/hir-ty/src/lower.rs new file mode 100644 index 00000000..5cc9a745 --- /dev/null +++ b/crates/hir-ty/src/lower.rs @@ -0,0 +1,697 @@ +//! Lowering from nameres-resolved HIR type references into semantic schemes. + +use std::cell::RefCell; + +use hir::{ + Db as HirDb, + anchor::DefId, + ast::{ + function::{FuncParam, FuncSig}, + item::{AdtCtor, AdtDef, ClassDef, FieldDef, FunctionDef, TypeAlias}, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + diag::LabelSpan, + nameres as hir_nameres, + span::Spanned, +}; +use rustc_hash::FxHashMap; + +use crate::{ + BoundTyVar, BuiltinClassId, BuiltinTyCtor, ClassId, Pred, QualTy, Ty, TyCtor, TyKind, TyScheme, + UserTyCtor, UserTyCtorKind, +}; + +/// Mapping from nameres type-variable binders to de Bruijn scheme indices. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BinderEnv<'db> { + binders: FxHashMap<(DefId<'db>, u32), BoundTyVar>, + binder_count: u32, +} + +/// Lowered function signature and the monomorphic pieces useful for body +/// inference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoweredFunction<'db> { + /// Polymorphic function scheme. + pub scheme: TyScheme<'db>, + /// Parameter types in source order. + pub params: Vec>, + /// Return type. + pub ret: Ty<'db>, +} + +/// Lowered field type scheme. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoweredField<'db> { + /// Field scheme. + pub scheme: TyScheme<'db>, + /// Field type. + pub ty: Ty<'db>, +} + +/// Lowered type-alias scheme. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoweredTypeAlias<'db> { + /// Alias scheme. + pub scheme: TyScheme<'db>, + /// Alias body type. + pub ty: Ty<'db>, +} + +/// Lowered ADT constructor scheme. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoweredAdtCtor<'db> { + /// Constructor scheme. + pub scheme: TyScheme<'db>, + /// Constructor field parameter types. + pub params: Vec>, + /// Constructed ADT result type. + pub ret: Ty<'db>, +} + +/// Ephemeral type-reference lowerer. +/// +/// The lowerer is built from nameres resolution records for one signature or +/// body. It never stores source spans in the resulting semantic types. +pub struct TypeLowering<'db> { + db: &'db dyn HirDb, + type_resolutions: FxHashMap, hir_nameres::Resolution<'db>>, + pred_resolutions: FxHashMap, hir_nameres::Resolution<'db>>, + binders: BinderEnv<'db>, + diagnostics: RefCell>, +} + +/// Diagnostic produced while lowering syntactically valid type references. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeLoweringDiagnostic { + /// A class name was resolved where a type constructor was required. + ClassAsType { + /// Source span for the class name. + span: LabelSpan, + /// Class name as written or resolved. + class: String, + }, +} + +impl<'db> BinderEnv<'db> { + /// Creates an empty binder environment. + pub fn empty() -> Self { + Self { + binders: FxHashMap::default(), + binder_count: 0, + } + } + + /// Builds a binder environment from nameres type-variable bindings. + pub fn from_type_vars(vars: &[hir_nameres::TypeVarBinding<'db>]) -> Self { + let mut binders = FxHashMap::default(); + for (scheme_index, var) in vars.iter().enumerate() { + binders.insert((var.owner, var.index), BoundTyVar::new(scheme_index as u32)); + } + Self { + binders, + binder_count: vars.len() as u32, + } + } + + /// Returns the number of binders in this scheme environment. + pub const fn binder_count(&self) -> u32 { + self.binder_count + } + + fn resolve_def_param(&self, def: DefId<'db>, index: u32) -> Option { + self.binders.get(&(def, index)).copied() + } + + fn resolve(&self, var: &hir_nameres::TypeVarId<'db>) -> Option { + self.binders.get(&(var.owner, var.index)).copied() + } +} + +impl<'db> TypeLowering<'db> { + /// Creates a lowerer from raw nameres resolution slices. + pub fn new( + db: &'db dyn HirDb, + types: &[hir_nameres::TypeResolution<'db>], + preds: &[hir_nameres::PredResolution<'db>], + binders: BinderEnv<'db>, + ) -> Self { + Self { + db, + type_resolutions: types + .iter() + .map(|entry| (entry.ty, entry.resolution.clone())) + .collect(), + pred_resolutions: preds + .iter() + .map(|entry| (entry.pred, entry.resolution.clone())) + .collect(), + binders, + diagnostics: RefCell::new(Vec::new()), + } + } + + /// Creates a lowerer from item-level resolution records. + pub fn from_item_resolutions( + db: &'db dyn HirDb, + map: &hir_nameres::ItemResolutionFacts<'db>, + binders: BinderEnv<'db>, + ) -> Self { + Self::new(db, &map.types, &map.preds, binders) + } + + /// Creates a lowerer from body-level resolution records. + pub fn from_body_resolutions( + db: &'db dyn HirDb, + map: &hir_nameres::BodyResolutionMap<'db>, + binders: BinderEnv<'db>, + ) -> Self { + Self::new(db, &map.types, &map.preds, binders) + } + + /// Lowers one type reference to a ground semantic type. + pub fn lower_type(&self, ty: TypeRef<'db>) -> Ty<'db> { + match ty.kind(self.db) { + TypeRefKind::Named { args, .. } => { + let Some(resolution) = self.type_resolutions.get(&ty) else { + return Ty::error(self.db); + }; + if let Some(bound) = self.lower_type_var_resolution(resolution) { + return Ty::bound(self.db, bound.index); + } + if let Some(class) = self.class_name_from_type_resolution(resolution) { + self.diagnostics + .borrow_mut() + .push(TypeLoweringDiagnostic::ClassAsType { + span: LabelSpan::from_span(self.db, ty.span(self.db)), + class, + }); + return Ty::error(self.db); + } + let Some(ctor) = self.lower_type_ctor_resolution(resolution) else { + return Ty::error(self.db); + }; + let args = args + .atom() + .iter() + .map(|arg| self.lower_type(*arg)) + .collect(); + Ty::named(self.db, ctor, args) + } + TypeRefKind::FixedArray { + element, length, .. + } => Ty::fixed_array(self.db, self.lower_type(*element), *length), + TypeRefKind::Fn { params, ret, .. } => { + // The existing checked-type model has no function qualifiers. + // Keep them losslessly in source HIR and erase them only at + // this explicit compatibility boundary until that semantic + // model grows qualifier-aware conversion and compatibility. + Ty::function( + self.db, + params + .atom() + .iter() + .map(|param| self.lower_type(*param)) + .collect(), + self.lower_type(*ret), + ) + } + TypeRefKind::Comptime { inner, .. } => Ty::comptime(self.db, self.lower_type(*inner)), + TypeRefKind::Tuple { elems } => product_ty( + self.db, + elems.atom().iter().map(|elem| self.lower_type(*elem)), + ), + TypeRefKind::Error { .. } => Ty::error(self.db), + } + } + + /// Drains diagnostics produced by previous lowering calls. + pub fn take_diagnostics(&self) -> Vec { + std::mem::take(&mut *self.diagnostics.borrow_mut()) + } + + /// Lowers one predicate reference to a semantic predicate. + pub fn lower_pred(&self, pred: PredRef<'db>) -> Pred<'db> { + let Some(resolution) = self.pred_resolutions.get(&pred) else { + return Pred::error(self.db); + }; + let Some(class) = self.lower_class_resolution(resolution) else { + return Pred::error(self.db); + }; + let kind = pred.kind(self.db); + Pred::in_class( + self.db, + class, + self.lower_type(kind.ty), + kind.args + .atom() + .iter() + .map(|arg| self.lower_type(*arg)) + .collect(), + ) + } + + /// Lowers a function signature to a scheme. + pub fn lower_func_sig(&self, sig: &FuncSig<'db>) -> LoweredFunction<'db> { + let params = sig + .params + .atom() + .iter() + .map(|param| self.lower_param(param)) + .collect::>(); + let ret = sig + .ret + .map(|ret| self.lower_type(ret)) + .unwrap_or_else(|| Ty::unit(self.db)); + let fn_ty = Ty::function(self.db, params.clone(), ret); + let preds = sig + .preds + .iter() + .map(|pred| self.lower_pred(*pred)) + .collect::>(); + let scheme = TyScheme::new( + self.db, + self.binders.binder_count(), + QualTy::new(self.db, preds, fn_ty), + ); + LoweredFunction { + scheme, + params, + ret, + } + } + + /// Lowers a function definition to a scheme. + pub fn lower_function(&self, function: FunctionDef<'db>) -> LoweredFunction<'db> { + self.lower_func_sig(function.sig(self.db)) + } + + /// Lowers a class method signature to the scheme visible at call sites. + /// + /// The method is qualified by the class head predicate, so instantiating + /// the scheme during body inference emits the pending class obligation + /// that a future solver will discharge. + pub fn lower_class_method(&self, class: ClassDef<'db>, method: &FuncSig<'db>) -> TyScheme<'db> { + let params = method + .params + .atom() + .iter() + .map(|param| self.lower_param(param)) + .collect::>(); + let ret = method + .ret + .map(|ret| self.lower_type(ret)) + .unwrap_or_else(|| Ty::unit(self.db)); + let mut preds = Vec::new(); + preds.push(self.lower_pred(class.head(self.db))); + preds.extend(method.preds.iter().map(|pred| self.lower_pred(*pred))); + TyScheme::new( + self.db, + self.binders.binder_count(), + QualTy::new(self.db, preds, Ty::function(self.db, params, ret)), + ) + } + + /// Lowers a type alias to a scheme. + pub fn lower_type_alias(&self, alias: TypeAlias<'db>) -> LoweredTypeAlias<'db> { + let ty = self.lower_type(alias.ty(self.db)); + let scheme = TyScheme::new( + self.db, + self.binders.binder_count(), + QualTy::monotype(self.db, ty), + ); + LoweredTypeAlias { scheme, ty } + } + + /// Lowers a field type to a scheme. + pub fn lower_field(&self, field: &FieldDef<'db>) -> LoweredField<'db> { + let ty = self.lower_type(field.ty()); + let scheme = TyScheme::new( + self.db, + self.binders.binder_count(), + QualTy::monotype(self.db, ty), + ); + LoweredField { scheme, ty } + } + + /// Lowers an ADT constructor to a function-like scheme. + pub fn lower_adt_ctor(&self, adt: AdtDef<'db>, ctor: &AdtCtor<'db>) -> LoweredAdtCtor<'db> { + let fields = self.lower_type(*ctor.fields.atom()); + let params = match ctor.field_count { + 0 => Vec::new(), + 1 => vec![fields], + _ => tuple_params(self.db, fields), + }; + let adt_def = adt.def_id_value(self.db); + let ret_args = adt + .ty_param_elems(self.db) + .iter() + .enumerate() + .map(|(index, _)| { + self.binders + .resolve_def_param(adt_def, index as u32) + .map(|bound| Ty::bound(self.db, bound.index)) + .unwrap_or_else(|| Ty::error(self.db)) + }) + .collect::>(); + let ret = Ty::named( + self.db, + TyCtor::User(UserTyCtor { + def: adt_def, + kind: UserTyCtorKind::Adt, + }), + ret_args, + ); + let ty = Ty::function(self.db, params.clone(), ret); + let scheme = TyScheme::new( + self.db, + self.binders.binder_count(), + QualTy::monotype(self.db, ty), + ); + LoweredAdtCtor { + scheme, + params, + ret, + } + } + + fn lower_param(&self, param: &FuncParam<'db>) -> Ty<'db> { + match param { + FuncParam::Typed { comptime, ty, .. } => { + self.maybe_comptime(*comptime, self.lower_type(*ty)) + } + FuncParam::Untyped { comptime, .. } => { + self.maybe_comptime(*comptime, Ty::unknown(self.db)) + } + FuncParam::Error { .. } => Ty::error(self.db), + } + } + + fn maybe_comptime(&self, marker: Option>, ty: Ty<'db>) -> Ty<'db> { + if marker.is_none() || matches!(ty.kind(self.db), TyKind::Comptime(_)) { + ty + } else { + Ty::comptime(self.db, ty) + } + } + + fn lower_type_var_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option { + match resolution { + hir_nameres::Resolution::Local(hir_nameres::LocalBinding::TypeVar(var)) => { + self.binders.resolve(var) + } + _ => None, + } + } + + fn lower_type_ctor_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option> { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Type(ty)) => { + Some(TyCtor::Builtin(builtin_type_ctor(*ty))) + } + hir_nameres::Resolution::Def { def, kind } => user_type_ctor(*def, *kind), + _ => None, + } + } + + fn class_name_from_type_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Class(class)) => { + Some(builtin_class_name(*class).to_owned()) + } + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } => Some(def.name(self.db).unwrap_or_else(|| "trait".to_owned())), + _ => None, + } + } + + fn lower_class_resolution( + &self, + resolution: &hir_nameres::Resolution<'db>, + ) -> Option> { + match resolution { + hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Class(class)) => { + Some(ClassId::Builtin(builtin_class(*class))) + } + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } => Some(ClassId::User(*def)), + _ => None, + } + } +} + +/// Returns the complete binder environment for a class method signature. +/// +/// Method-local generic parameters are indexed after the enclosing trait +/// binders because both are owned by the trait definition in name resolution. +pub fn class_method_type_vars<'db>( + db: &'db dyn HirDb, + class: ClassDef<'db>, + method: &FuncSig<'db>, +) -> Vec> { + let owner = class.def_id_value(db); + let mut vars = hir_nameres::type_var_bindings(owner, class.type_var_elems(db)); + vars.extend(hir_nameres::type_var_bindings_from( + owner, + class.type_var_elems(db).len() as u32, + &method.type_vars, + )); + vars +} + +/// Returns the builtin value scheme for a resolved builtin term or class +/// method. +pub fn builtin_scheme<'db>( + db: &'db dyn HirDb, + builtin: hir_nameres::BuiltinKind, +) -> Option> { + match builtin { + hir_nameres::BuiltinKind::Constructor(ctor) => builtin_ctor_scheme(db, ctor), + hir_nameres::BuiltinKind::Function(function) => builtin_function_scheme(db, function), + hir_nameres::BuiltinKind::ClassMethod(method) => builtin_method_scheme(db, method), + hir_nameres::BuiltinKind::Type(_) | hir_nameres::BuiltinKind::Class(_) => None, + } +} + +fn builtin_ctor_scheme<'db>( + db: &'db dyn HirDb, + ctor: hir_nameres::BuiltinCtor, +) -> Option> { + let ty = match ctor { + hir_nameres::BuiltinCtor::True | hir_nameres::BuiltinCtor::False => Ty::bool(db), + hir_nameres::BuiltinCtor::Unit => Ty::unit(db), + hir_nameres::BuiltinCtor::Pair => { + let lhs = Ty::bound(db, 0); + let rhs = Ty::bound(db, 1); + let pair = Ty::named(db, TyCtor::Builtin(BuiltinTyCtor::Pair), vec![lhs, rhs]); + return Some(TyScheme::new( + db, + 2, + QualTy::monotype(db, Ty::function(db, vec![lhs, rhs], pair)), + )); + } + hir_nameres::BuiltinCtor::Inl => { + let lhs = Ty::bound(db, 0); + let rhs = Ty::bound(db, 1); + let sum = Ty::named(db, TyCtor::Builtin(BuiltinTyCtor::Sum), vec![lhs, rhs]); + return Some(TyScheme::new( + db, + 2, + QualTy::monotype(db, Ty::function(db, vec![lhs], sum)), + )); + } + hir_nameres::BuiltinCtor::Inr => { + let lhs = Ty::bound(db, 0); + let rhs = Ty::bound(db, 1); + let sum = Ty::named(db, TyCtor::Builtin(BuiltinTyCtor::Sum), vec![lhs, rhs]); + return Some(TyScheme::new( + db, + 2, + QualTy::monotype(db, Ty::function(db, vec![rhs], sum)), + )); + } + }; + Some(TyScheme::monotype(db, ty)) +} + +fn builtin_function_scheme<'db>( + db: &'db dyn HirDb, + function: hir_nameres::BuiltinFunction, +) -> Option> { + let word = Ty::word(db); + let integer = Ty::integer(db); + let bool_ty = Ty::bool(db); + let scheme = match function { + hir_nameres::BuiltinFunction::PrimAddWord => { + TyScheme::monotype(db, Ty::function(db, vec![word, word], word)) + } + hir_nameres::BuiltinFunction::PrimEqWord => { + TyScheme::monotype(db, Ty::function(db, vec![word, word], word)) + } + hir_nameres::BuiltinFunction::WordToInteger => { + TyScheme::monotype(db, Ty::function(db, vec![word], integer)) + } + hir_nameres::BuiltinFunction::WordFromInteger => { + TyScheme::monotype(db, Ty::function(db, vec![integer], word)) + } + hir_nameres::BuiltinFunction::IntegerAdd + | hir_nameres::BuiltinFunction::IntegerSub + | hir_nameres::BuiltinFunction::IntegerMul => { + TyScheme::monotype(db, Ty::function(db, vec![integer, integer], integer)) + } + hir_nameres::BuiltinFunction::IntegerLt | hir_nameres::BuiltinFunction::IntegerEq => { + TyScheme::monotype(db, Ty::function(db, vec![integer, integer], bool_ty)) + } + hir_nameres::BuiltinFunction::Invoke => return Some(invokable_invoke_scheme(db)), + }; + Some(scheme) +} + +fn builtin_method_scheme<'db>( + db: &'db dyn HirDb, + method: hir_nameres::BuiltinClassMethod, +) -> Option> { + match method { + hir_nameres::BuiltinClassMethod::IntFromInteger => { + let result = Ty::bound(db, 0); + let pred = Pred::in_class( + db, + ClassId::Builtin(BuiltinClassId::Int), + result, + Vec::new(), + ); + Some(TyScheme::new( + db, + 1, + QualTy::new( + db, + vec![pred], + Ty::function(db, vec![Ty::integer(db)], result), + ), + )) + } + hir_nameres::BuiltinClassMethod::InvokableInvoke => Some(invokable_invoke_scheme(db)), + } +} + +fn invokable_invoke_scheme<'db>(db: &'db dyn HirDb) -> TyScheme<'db> { + let self_ty = Ty::bound(db, 0); + let args = Ty::bound(db, 1); + let ret = Ty::bound(db, 2); + let pred = Pred::in_class( + db, + ClassId::Builtin(BuiltinClassId::Invokable), + self_ty, + vec![args, ret], + ); + TyScheme::new( + db, + 3, + QualTy::new(db, vec![pred], Ty::function(db, vec![self_ty, args], ret)), + ) +} + +fn builtin_type_ctor(ty: hir_nameres::BuiltinType) -> BuiltinTyCtor { + match ty { + hir_nameres::BuiltinType::Word => BuiltinTyCtor::Word, + hir_nameres::BuiltinType::Bool => BuiltinTyCtor::Bool, + hir_nameres::BuiltinType::String => BuiltinTyCtor::String, + hir_nameres::BuiltinType::Unit => BuiltinTyCtor::Unit, + hir_nameres::BuiltinType::Pair => BuiltinTyCtor::Pair, + hir_nameres::BuiltinType::Sum => BuiltinTyCtor::Sum, + hir_nameres::BuiltinType::Integer => BuiltinTyCtor::Integer, + } +} + +fn builtin_class(class: hir_nameres::BuiltinClass) -> BuiltinClassId { + match class { + hir_nameres::BuiltinClass::Invokable => BuiltinClassId::Invokable, + hir_nameres::BuiltinClass::Int => BuiltinClassId::Int, + } +} + +fn builtin_class_name(class: hir_nameres::BuiltinClass) -> &'static str { + match class { + hir_nameres::BuiltinClass::Invokable => "invokable", + hir_nameres::BuiltinClass::Int => "Int", + } +} + +fn user_type_ctor<'db>( + def: DefId<'db>, + kind: hir_nameres::DefResolutionKind, +) -> Option> { + let kind = match kind { + hir_nameres::DefResolutionKind::Adt => UserTyCtorKind::Adt, + hir_nameres::DefResolutionKind::TypeAlias => UserTyCtorKind::Alias, + hir_nameres::DefResolutionKind::ValueType => UserTyCtorKind::ValueType, + hir_nameres::DefResolutionKind::Contract => UserTyCtorKind::Contract, + hir_nameres::DefResolutionKind::Function + | hir_nameres::DefResolutionKind::Class + | hir_nameres::DefResolutionKind::Instance => return None, + }; + Some(TyCtor::User(UserTyCtor { def, kind })) +} + +fn tuple_params<'db>(db: &'db dyn HirDb, ty: Ty<'db>) -> Vec> { + match ty.kind(db) { + TyKind::Tuple(elems) => elems.clone(), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Vec::new(), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => { + let mut params = Vec::new(); + params.push(args[0]); + push_product_tail_params(db, args[1], &mut params); + params + } + _ => vec![ty], + } +} + +fn product_ty<'db>(db: &'db dyn HirDb, elems: impl IntoIterator>) -> Ty<'db> { + let mut elems = elems.into_iter(); + let Some(head) = elems.next() else { + return Ty::unit(db); + }; + let tail = elems.collect::>(); + if tail.is_empty() { + head + } else { + Ty::named( + db, + TyCtor::Builtin(BuiltinTyCtor::Pair), + vec![head, product_ty(db, tail)], + ) + } +} + +fn push_product_tail_params<'db>(db: &'db dyn HirDb, ty: Ty<'db>, out: &mut Vec>) { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => { + out.push(args[0]); + push_product_tail_params(db, args[1], out); + } + _ => out.push(ty), + } +} diff --git a/crates/hir-ty/src/prepare.rs b/crates/hir-ty/src/prepare.rs new file mode 100644 index 00000000..ab85f9f9 --- /dev/null +++ b/crates/hir-ty/src/prepare.rs @@ -0,0 +1,2200 @@ +//! Compiler-owned HIR overlays built before type checking. +//! +//! Preparation never changes or reparses the user's [`hir::input::SourceFile`]. It keeps +//! the parsed module as the source-of-record and constructs a second tracked +//! [`Module`] containing unresolved generated declarations. Name resolution, +//! type checking, and specialization can therefore process generated code by +//! the same paths as source HIR while editor-facing consumers keep using the +//! source module. +//! +//! The overlay is intentionally the home for declaration-level rewrites that +//! create names, instances, or cross-body entry points. Body-local tuple, +//! boolean, conditional, and implicit-return desugars remain tracked views so +//! a small body edit does not rebuild a whole module. Contract constructor and +//! deployment wrappers live here alongside runtime dispatch; derived instances +//! and semantic field/call hooks can migrate here once their required +//! resolution inputs are one-way dependencies. + +use std::collections::BTreeSet; + +use hir::{ + anchor::{DefId, DefKind, Disambiguator}, + arena::{Arena, Id}, + ast::{ + Ident, + function::{ + AssignOp, Expr, ExprKind, FuncBody, FuncParam, FuncSig, LitKind, MatchArm, Pat, + PatKind, Stmt, StmtKind, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind, + }, + item::{ + AdtDef, ContractDef, ContractItem, ContractKind, FuncKind, FunctionDef, InstanceDef, + Item, Module, + }, + ty::{PredRef, PredRefKind, TypeRef, TypeRefKind}, + }, + nameres::{compiler_contract_method_ref, ident_text}, + span::{Span, Spanned, SpannedElem}, +}; + +use crate::{Db, contract_needs_generated_dispatch}; + +const GENERATED_MAIN_NAME: &str = "$solcore$dispatch$main"; +const MAIN_FINGERPRINT: &str = "solcore.generated.std_dispatch.main"; +const MAIN_BODY_FINGERPRINT: &str = "solcore.generated.std_dispatch.main.body"; +const CONSTRUCTOR_INIT_NAME: &str = "$solcore$constructor$init_"; +const CONSTRUCTOR_COPY_NAME: &str = "$solcore$constructor$copy_arguments_for_constructor"; +const DEPLOYMENT_MAIN_NAME: &str = "$solcore$constructor$start"; +const CONSTRUCTOR_INIT_FINGERPRINT: &str = "solcore.generated.constructor.init"; +const CONSTRUCTOR_INIT_BODY_FINGERPRINT: &str = "solcore.generated.constructor.init.body"; +const CONSTRUCTOR_COPY_FINGERPRINT: &str = "solcore.generated.constructor.copy_arguments"; +const CONSTRUCTOR_COPY_BODY_FINGERPRINT: &str = "solcore.generated.constructor.copy_arguments.body"; +const DEPLOYMENT_MAIN_FINGERPRINT: &str = "solcore.generated.constructor.deployment_main"; +const DEPLOYMENT_MAIN_BODY_FINGERPRINT: &str = "solcore.generated.constructor.deployment_main.body"; + +/// A source module paired with its compiler-prepared HIR overlay. +/// +/// `source` and `module` intentionally remain separate even though their +/// module [`DefId`] values are equal. The tracked `Module` handles distinguish +/// source-only queries from prepared semantic queries; consumers must not use +/// only the module `DefId` as a cache key when both can be present. +#[salsa::tracked(debug)] +pub struct PreparedModule<'db> { + /// Parsed, user-authored HIR. LSP and source diagnostics use this module. + #[tracked] + #[returns(copy)] + pub source: Module<'db>, + + /// Effective HIR consumed by name resolution, type checking, and backends. + #[tracked] + #[returns(copy)] + pub module: Module<'db>, + + /// Provenance for compiler-owned definitions in `module`. + #[tracked] + #[returns(ref)] + pub origins: GeneratedOriginMap<'db>, +} + +impl<'db> PreparedModule<'db> { + /// Returns the generated origin for `def`, if it belongs to this overlay. + pub fn origin_for_def( + self, + db: &'db dyn Db, + def: DefId<'db>, + ) -> Option<&'db GeneratedOrigin<'db>> { + self.origins(db).origin_for_def(def) + } + + /// Returns the compiler-owned runtime main for `contract`, if generated. + pub fn contract_dispatch_main( + self, + db: &'db dyn Db, + contract: DefId<'db>, + ) -> Option> { + self.origins(db).contract_dispatch_main(contract) + } + + /// Returns the compiler-owned deployment entry for `contract`, if generated. + pub fn contract_deployment_main( + self, + db: &'db dyn Db, + contract: DefId<'db>, + ) -> Option> { + self.origins(db).contract_deployment_main(contract) + } +} + +/// Why a definition exists only in a prepared module. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum GeneratedOriginKind { + /// Compiler-owned contract runtime entry backed by `std.dispatch`. + ContractDispatchMain, + /// Phantom method-name data type used by `std.dispatch.Method`. + ContractDispatchNameType, + /// `SigString` instance for a phantom method-name type. + ContractDispatchSigStringInstance, + /// `sigStr` method (or its body) in a generated `SigString` instance. + ContractDispatchSigStringMethod, + /// Private compiler-only function containing the source constructor body. + ContractConstructorInit, + /// Private constructor-argument copying and ABI-decoding helper. + ContractConstructorCopyArguments, + /// Compiler-owned deployment entry that installs the runtime object. + ContractDeploymentMain, +} + +/// Returns whether `def` is a compiler-owned std.dispatch runtime entry. +/// +/// The fingerprint fallback keeps provenance available to consumers that are +/// handed an already-prepared [`Module`] without its [`PreparedModule`] +/// wrapper. Source-lowered functions never receive this reserved fingerprint. +pub fn is_contract_dispatch_main_def(db: &dyn Db, def: DefId<'_>) -> bool { + def.kind(db) == DefKind::Function && def.fingerprint(db).as_deref() == Some(MAIN_FINGERPRINT) +} + +/// Returns whether `def` is a compiler-owned contract deployment entry. +/// +/// Like [`is_contract_dispatch_main_def`], the fingerprint fallback supports +/// consumers handed an already-prepared module without its wrapper. +pub fn is_contract_deployment_main_def(db: &dyn Db, def: DefId<'_>) -> bool { + def.kind(db) == DefKind::Function + && def.fingerprint(db).as_deref() == Some(DEPLOYMENT_MAIN_FINGERPRINT) +} + +/// Stable backend spelling for a compiler-private contract overlay function. +/// +/// Effective-HIR names are intentionally impossible to spell in source so +/// they cannot capture user references. Backends may retain the established +/// readable names because their qualified names also carry a DefId hash. +pub fn contract_overlay_backend_name(db: &dyn Db, def: DefId<'_>) -> Option<&'static str> { + if def.kind(db) != DefKind::Function { + return None; + } + match def.fingerprint(db).as_deref()? { + MAIN_FINGERPRINT => Some("main"), + CONSTRUCTOR_INIT_FINGERPRINT => Some("init_"), + CONSTRUCTOR_COPY_FINGERPRINT => Some("copy_arguments_for_constructor"), + DEPLOYMENT_MAIN_FINGERPRINT => Some("_start"), + _ => None, + } +} + +/// User-source provenance for one compiler-owned definition. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct GeneratedOrigin<'db> { + pub def: DefId<'db>, + pub kind: GeneratedOriginKind, + pub contract: DefId<'db>, + pub method: Option>, + pub span: Span<'db>, +} + +/// Deterministically ordered generated-definition provenance. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, salsa::Update)] +pub struct GeneratedOriginMap<'db> { + entries: Vec>, +} + +impl<'db> GeneratedOriginMap<'db> { + pub fn entries(&self) -> &[GeneratedOrigin<'db>] { + &self.entries + } + + pub fn origin_for_def(&self, def: DefId<'db>) -> Option<&GeneratedOrigin<'db>> { + self.entries.iter().find(|origin| origin.def == def) + } + + pub fn contract_dispatch_main(&self, contract: DefId<'db>) -> Option> { + self.entries.iter().find_map(|origin| { + (origin.contract == contract + && origin.kind == GeneratedOriginKind::ContractDispatchMain) + .then_some(origin.def) + }) + } + + pub fn contract_deployment_main(&self, contract: DefId<'db>) -> Option> { + self.entries.iter().find_map(|origin| { + (origin.contract == contract + && origin.kind == GeneratedOriginKind::ContractDeploymentMain) + .then_some(origin.def) + }) + } + + fn extend(&mut self, origins: impl IntoIterator>) { + self.entries.extend(origins); + } +} + +/// Builds the pre-typecheck HIR overlay for `source`. +/// +/// This query is deliberately source-only: it inspects explicit function +/// signatures, but never calls body inference, +/// module type checking, or specialization. That one-way dependency avoids +/// `typeck -> prepare -> typeck` query cycles. Generated contract entries use +/// the same unqualified standard-library names as the reference compiler and +/// therefore rely on the source module's explicit imports. +#[salsa::tracked] +pub fn prepare_module<'db>(db: &'db dyn Db, source: Module<'db>) -> PreparedModule<'db> { + let module_def = source.def_id_value(db); + let mut generated_items = Vec::new(); + let mut prepared_source_items = Vec::with_capacity(source.items(db).len()); + let mut origins = GeneratedOriginMap::default(); + + for item in source.items(db) { + let Item::ContractDef(contract) = *item else { + prepared_source_items.push(*item); + continue; + }; + if contract.kind(db) != ContractKind::Contract { + prepared_source_items.push(*item); + continue; + } + + let mut prepared_contract = contract; + if !contract_has_prepared_constructor(db, contract) + && let Some(artifacts) = prepare_contract_constructor(db, prepared_contract) + { + origins.extend(artifacts.origins.iter().cloned()); + prepared_contract = artifacts.contract; + } + + if contract_needs_generated_dispatch(db, contract) + && !contract_has_prepared_dispatch(db, prepared_contract) + && let Some(artifacts) = prepare_contract_dispatch(db, module_def, prepared_contract) + { + generated_items.extend(artifacts.top_level_items.iter().copied()); + origins.extend(artifacts.origins.iter().cloned()); + prepared_contract = artifacts.contract; + } + prepared_source_items.push(Item::ContractDef(prepared_contract)); + } + + if origins.entries.is_empty() { + return PreparedModule::new(db, source, source, origins); + } + + generated_items.extend(prepared_source_items); + let effective = Module::new( + db, + source.def_id_value(db), + source.span(db), + generated_items, + ); + PreparedModule::new(db, source, effective, origins) +} + +fn contract_has_prepared_constructor(db: &dyn Db, contract: ContractDef<'_>) -> bool { + contract.items(db).iter().any(|item| { + matches!( + item, + ContractItem::FunctionDef(function) + if is_contract_deployment_main_def(db, function.def_id_value(db)) + ) + }) +} + +fn contract_has_prepared_dispatch(db: &dyn Db, contract: ContractDef<'_>) -> bool { + contract.items(db).iter().any(|item| { + matches!( + item, + ContractItem::FunctionDef(function) + if is_contract_dispatch_main_def(db, function.def_id_value(db)) + ) + }) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +struct PreparedContractArtifacts<'db> { + contract: ContractDef<'db>, + top_level_items: Vec>, + origins: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +struct RawMethod<'db> { + def: DefId<'db>, + name: String, + span: Span<'db>, + payable: bool, + params: Vec>, + ret: TypeRef<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +struct RawFallback<'db> { + name: String, + payable: bool, + params: Vec>, + ret: TypeRef<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +struct RawConstructor<'db> { + def: Option>, + span: Span<'db>, + params: Vec>, + param_types: Vec>, + body: Option>, + payable: bool, +} + +#[salsa::tracked] +fn prepare_contract_constructor<'db>( + db: &'db dyn Db, + contract: ContractDef<'db>, +) -> Option> { + let constructors = contract + .items(db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) if function.kind(db) == FuncKind::Constructor => { + Some(*function) + } + _ => None, + }) + .collect::>(); + if constructors.len() > 1 { + // Source diagnostics own the duplicate-constructor error. Do not add a + // deployment entry to structurally invalid recovery input. + return None; + } + + let span = constructors.first().map_or_else( + || contract.name_elem(db).span(db), + |function| function.span(db), + ); + let constructor = if let Some(function) = constructors.first().copied() { + let sig = function.sig(db); + RawConstructor { + def: Some(function.def_id_value(db)), + span, + params: sig.params.atom().clone(), + param_types: explicit_param_types(sig.params.atom())?, + body: function.body(db), + payable: sig.is_payable(), + } + } else { + RawConstructor { + def: None, + span, + params: Vec::new(), + param_types: Vec::new(), + body: None, + payable: false, + } + }; + + let contract_def = contract.def_id_value(db); + let contract_name = ident_text(db, &contract.name_elem(db)); + let (init, mut init_origins) = generated_constructor_init(db, contract_def, &constructor); + let (copy, mut copy_origins) = + generated_constructor_copy_arguments(db, contract_def, &contract_name, &constructor); + let (start, mut start_origins) = + generated_deployment_main(db, contract_def, &contract_name, &constructor); + + let mut contract_items = contract + .items(db) + .iter() + .filter(|item| { + !matches!( + item, + ContractItem::FunctionDef(function) + if function.kind(db) == FuncKind::Constructor + ) + }) + .copied() + .collect::>(); + contract_items.extend([ + ContractItem::FunctionDef(init), + ContractItem::FunctionDef(copy), + ContractItem::FunctionDef(start), + ]); + let prepared_contract = ContractDef::new( + db, + contract_def, + contract.span(db), + contract.leading_comments(db).clone(), + contract.kind(db), + contract.name_elem(db), + contract.ty_param_elems(db).clone(), + contract.fields(db).clone(), + contract.field_comments(db).clone(), + contract_items, + ); + let mut origins = Vec::new(); + origins.append(&mut init_origins); + origins.append(&mut copy_origins); + origins.append(&mut start_origins); + Some(PreparedContractArtifacts { + contract: prepared_contract, + top_level_items: Vec::new(), + origins, + }) +} + +fn generated_constructor_init<'db>( + db: &'db dyn Db, + contract: DefId<'db>, + constructor: &RawConstructor<'db>, +) -> (FunctionDef<'db>, Vec>) { + let function_def = generated_def( + db, + contract, + DefKind::Function, + CONSTRUCTOR_INIT_NAME, + CONSTRUCTOR_INIT_FINGERPRINT, + ); + let body_def = generated_def( + db, + function_def, + DefKind::FuncBody, + CONSTRUCTOR_INIT_NAME, + CONSTRUCTOR_INIT_BODY_FINGERPRINT, + ); + let body = constructor.body.map_or_else( + || generated_unit_body(db, body_def, constructor.span), + |source| clone_body_with_def(db, source, body_def), + ); + let sig = FuncSig { + span: constructor.span, + type_vars: Vec::new(), + preds: Vec::new(), + visibility: None, + mutability: None, + name: spanned_ident(db, constructor.span, CONSTRUCTOR_INIT_NAME), + params: SpannedElem::new(constructor.params.clone(), constructor.span), + ret: Some(unit_ty(db, constructor.span)), + ret_names: Vec::new(), + }; + let function = FunctionDef::new( + db, + function_def, + constructor.span, + FuncKind::Function, + Vec::new(), + sig, + Some(body), + ); + ( + function, + generated_function_origins( + function_def, + body_def, + GeneratedOriginKind::ContractConstructorInit, + contract, + constructor.def, + constructor.span, + ), + ) +} + +fn generated_constructor_copy_arguments<'db>( + db: &'db dyn Db, + contract: DefId<'db>, + contract_name: &str, + constructor: &RawConstructor<'db>, +) -> (FunctionDef<'db>, Vec>) { + let function_def = generated_def( + db, + contract, + DefKind::Function, + CONSTRUCTOR_COPY_NAME, + CONSTRUCTOR_COPY_FINGERPRINT, + ); + let body_def = generated_def( + db, + function_def, + DefKind::FuncBody, + CONSTRUCTOR_COPY_NAME, + CONSTRUCTOR_COPY_BODY_FINGERPRINT, + ); + let args_ty = product_ty(db, constructor.span, &constructor.param_types); + let mut builder = BodyBuilder::new(db, constructor.span); + let top_level_stmts = if constructor.params.is_empty() { + let unit = builder.alloc_expr(ExprKind::Tuple(Vec::new())); + vec![builder.alloc_stmt(StmtKind::Return(Some(unit)))] + } else { + let res = builder.let_stmt("res", Some(args_ty), None); + let memory_offset = builder.let_stmt( + "memoryDataOffset", + Some(named_ty(db, constructor.span, "word", Vec::new())), + None, + ); + let copy = builder.alloc_stmt(StmtKind::Assembly { + body: constructor_copy_yul(db, constructor.span, contract_name), + }); + let offset = builder.ident("memoryDataOffset"); + let memory_value = builder.call_path(&["memory", "memory"], vec![offset]); + let source_ty = named_ty( + db, + constructor.span, + "memory", + vec![named_ty(db, constructor.span, "bytes", Vec::new())], + ); + let source = builder.let_stmt("source", Some(source_ty), Some(memory_value)); + let source_expr = builder.ident("source"); + let args_proxy = builder.proxy(args_ty); + let reader_proxy = builder.proxy(named_ty( + db, + constructor.span, + "MemoryWordReader", + Vec::new(), + )); + let decoded = builder.call_ident("abi_decode", vec![source_expr, args_proxy, reader_proxy]); + let lhs = builder.ident("res"); + let assign = builder.alloc_stmt(StmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs: decoded, + }); + let result = builder.ident("res"); + let ret = builder.alloc_stmt(StmtKind::Return(Some(result))); + vec![res, memory_offset, copy, source, assign, ret] + }; + let (stmts, exprs, pats) = builder.finish(); + let body = FuncBody::new( + db, + body_def, + constructor.span, + top_level_stmts, + stmts, + exprs, + pats, + ); + let sig = FuncSig { + span: constructor.span, + type_vars: Vec::new(), + preds: Vec::new(), + visibility: None, + mutability: None, + name: spanned_ident(db, constructor.span, CONSTRUCTOR_COPY_NAME), + params: SpannedElem::new(Vec::new(), constructor.span), + ret: Some(args_ty), + ret_names: Vec::new(), + }; + let function = FunctionDef::new( + db, + function_def, + constructor.span, + FuncKind::Function, + Vec::new(), + sig, + Some(body), + ); + ( + function, + generated_function_origins( + function_def, + body_def, + GeneratedOriginKind::ContractConstructorCopyArguments, + contract, + constructor.def, + constructor.span, + ), + ) +} + +fn generated_deployment_main<'db>( + db: &'db dyn Db, + contract: DefId<'db>, + contract_name: &str, + constructor: &RawConstructor<'db>, +) -> (FunctionDef<'db>, Vec>) { + let function_def = generated_def( + db, + contract, + DefKind::Function, + DEPLOYMENT_MAIN_NAME, + DEPLOYMENT_MAIN_FINGERPRINT, + ); + let body_def = generated_def( + db, + function_def, + DefKind::FuncBody, + DEPLOYMENT_MAIN_NAME, + DEPLOYMENT_MAIN_BODY_FINGERPRINT, + ); + let args_ty = product_ty(db, constructor.span, &constructor.param_types); + let mut builder = BodyBuilder::new(db, constructor.span); + let mut top_level_stmts = vec![builder.alloc_stmt(StmtKind::Assembly { + body: deployment_setup_yul(db, constructor.span, contract_name), + })]; + if !constructor.payable { + top_level_stmts.push(builder.alloc_stmt(StmtKind::Assembly { + body: nonpayable_constructor_yul(db, constructor.span), + })); + } + let copy_args = builder.call_ident(CONSTRUCTOR_COPY_NAME, Vec::new()); + top_level_stmts.push(builder.let_stmt("conargs", Some(args_ty), Some(copy_args))); + // Haskell invokes its constructor init helper indirectly with the product. Rust's frontend can + // express the same operation without dictionary evidence: destructure a + // multi-argument product, then make an ordinary direct call. + match constructor.params.len() { + 0 => { + let invoke = builder.call_ident(CONSTRUCTOR_INIT_NAME, Vec::new()); + top_level_stmts.push(builder.alloc_stmt(StmtKind::Expr(invoke))); + } + 1 => { + let conargs = builder.ident("conargs"); + let invoke = builder.call_ident(CONSTRUCTOR_INIT_NAME, vec![conargs]); + top_level_stmts.push(builder.alloc_stmt(StmtKind::Expr(invoke))); + } + len => { + let names = (0..len) + .map(|index| format!("conarg{index}")) + .collect::>(); + let pat = builder.product_pat(&names); + let args = names.iter().map(|name| builder.ident(name)).collect(); + let invoke = builder.call_ident(CONSTRUCTOR_INIT_NAME, args); + let invoke = builder.alloc_stmt(StmtKind::Expr(invoke)); + let conargs = builder.ident("conargs"); + top_level_stmts.push(builder.alloc_stmt(StmtKind::Match { + scrutinees: vec![conargs], + arms: vec![MatchArm { + span: constructor.span, + pats: vec![pat], + body: vec![invoke], + }], + })); + } + } + top_level_stmts.push(builder.alloc_stmt(StmtKind::Assembly { + body: return_runtime_object_yul(db, constructor.span, contract_name), + })); + let (stmts, exprs, pats) = builder.finish(); + let body = FuncBody::new( + db, + body_def, + constructor.span, + top_level_stmts, + stmts, + exprs, + pats, + ); + let sig = FuncSig { + span: constructor.span, + type_vars: Vec::new(), + preds: Vec::new(), + visibility: None, + mutability: None, + name: spanned_ident(db, constructor.span, DEPLOYMENT_MAIN_NAME), + params: SpannedElem::new(Vec::new(), constructor.span), + ret: Some(unit_ty(db, constructor.span)), + ret_names: Vec::new(), + }; + let function = FunctionDef::new( + db, + function_def, + constructor.span, + FuncKind::Function, + Vec::new(), + sig, + Some(body), + ); + ( + function, + generated_function_origins( + function_def, + body_def, + GeneratedOriginKind::ContractDeploymentMain, + contract, + constructor.def, + constructor.span, + ), + ) +} + +fn generated_function_origins<'db>( + function: DefId<'db>, + body: DefId<'db>, + kind: GeneratedOriginKind, + contract: DefId<'db>, + source: Option>, + span: Span<'db>, +) -> Vec> { + vec![ + GeneratedOrigin { + def: function, + kind, + contract, + method: source, + span, + }, + GeneratedOrigin { + def: body, + kind, + contract, + method: source, + span, + }, + ] +} + +fn clone_body_with_def<'db>( + db: &'db dyn Db, + source: FuncBody<'db>, + def: DefId<'db>, +) -> FuncBody<'db> { + FuncBody::new( + db, + def, + source.span(db), + source.top_level_stmts(db).clone(), + source.stmts(db).clone(), + source.exprs(db).clone(), + source.pats(db).clone(), + ) +} + +fn generated_unit_body<'db>(db: &'db dyn Db, def: DefId<'db>, span: Span<'db>) -> FuncBody<'db> { + let mut builder = BodyBuilder::new(db, span); + let unit = builder.alloc_expr(ExprKind::Tuple(Vec::new())); + let ret = builder.alloc_stmt(StmtKind::Return(Some(unit))); + let (stmts, exprs, pats) = builder.finish(); + FuncBody::new(db, def, span, vec![ret], stmts, exprs, pats) +} + +fn constructor_copy_yul<'db>( + db: &'db dyn Db, + span: Span<'db>, + contract_name: &str, +) -> Vec> { + let deployer = format!("{contract_name}Deploy"); + let program_size = yul_call(db, span, "datasize", vec![yul_string(span, &deployer)]); + let codesize = yul_call(db, span, "codesize", Vec::new()); + let arg_size = yul_call( + db, + span, + "sub", + vec![codesize, yul_ident_expr(db, span, "programSize")], + ); + let free_ptr = yul_call(db, span, "mload", vec![yul_number(span, "64")]); + let new_free_ptr = yul_call( + db, + span, + "add", + vec![ + yul_ident_expr(db, span, "memoryDataOffset"), + yul_ident_expr(db, span, "argSize"), + ], + ); + let update_free_ptr = yul_call( + db, + span, + "mstore", + vec![yul_number(span, "64"), new_free_ptr], + ); + let copy = yul_call( + db, + span, + "codecopy", + vec![ + yul_ident_expr(db, span, "memoryDataOffset"), + yul_ident_expr(db, span, "programSize"), + yul_ident_expr(db, span, "argSize"), + ], + ); + vec![ + yul_let(db, span, "programSize", Some(program_size)), + yul_let(db, span, "argSize", Some(arg_size)), + yul_assign(db, span, "memoryDataOffset", free_ptr), + yul_expr_stmt(span, update_free_ptr), + yul_expr_stmt(span, copy), + ] +} + +fn deployment_setup_yul<'db>( + db: &'db dyn Db, + span: Span<'db>, + contract_name: &str, +) -> Vec> { + let deployer = format!("{contract_name}Deploy"); + let memoryguard = yul_call(db, span, "memoryguard", vec![yul_number(span, "128")]); + let initialize_memory = yul_call( + db, + span, + "mstore", + vec![yul_number(span, "64"), memoryguard], + ); + let codesize = yul_call(db, span, "codesize", Vec::new()); + let deployer_size = yul_call(db, span, "datasize", vec![yul_string(span, &deployer)]); + let truncated = yul_call(db, span, "lt", vec![codesize, deployer_size]); + let revert = yul_call( + db, + span, + "revert", + vec![yul_number(span, "0"), yul_number(span, "0")], + ); + vec![ + yul_expr_stmt(span, initialize_memory), + YulStmt { + span, + kind: YulStmtKind::If { + cond: truncated, + body: vec![yul_expr_stmt(span, revert)], + }, + }, + ] +} + +fn nonpayable_constructor_yul<'db>(db: &'db dyn Db, span: Span<'db>) -> Vec> { + let callvalue = yul_call(db, span, "callvalue", Vec::new()); + let store_selector = yul_call( + db, + span, + "mstore", + vec![yul_number(span, "0"), yul_hex(span, "0xb5988ea3")], + ); + let revert = yul_call( + db, + span, + "revert", + vec![yul_number(span, "28"), yul_number(span, "4")], + ); + vec![YulStmt { + span, + kind: YulStmtKind::If { + cond: callvalue, + body: vec![ + yul_expr_stmt(span, store_selector), + yul_expr_stmt(span, revert), + ], + }, + }] +} + +fn return_runtime_object_yul<'db>( + db: &'db dyn Db, + span: Span<'db>, + contract_name: &str, +) -> Vec> { + let runtime = yul_string(span, contract_name); + let size = yul_call(db, span, "datasize", vec![runtime.clone()]); + let offset = yul_call(db, span, "dataoffset", vec![runtime.clone()]); + let runtime_size = yul_call(db, span, "datasize", vec![runtime]); + let copy = yul_call( + db, + span, + "codecopy", + vec![yul_number(span, "0"), offset, runtime_size], + ); + let ret = yul_call( + db, + span, + "return", + vec![yul_number(span, "0"), yul_ident_expr(db, span, "size")], + ); + vec![ + yul_let(db, span, "size", Some(size)), + yul_expr_stmt(span, copy), + yul_expr_stmt(span, ret), + ] +} + +fn yul_ident<'db>(db: &'db dyn Db, span: Span<'db>, name: &str) -> SpannedElem<'db, Ident<'db>> { + spanned_ident(db, span, name) +} + +fn yul_ident_expr<'db>(db: &'db dyn Db, span: Span<'db>, name: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Ident(yul_ident(db, span, name)), + } +} + +fn yul_number<'db>(span: Span<'db>, value: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Lit(YulLitKind::Number(value.to_owned())), + } +} + +fn yul_hex<'db>(span: Span<'db>, value: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Lit(YulLitKind::Hex(value.to_owned())), + } +} + +fn yul_string<'db>(span: Span<'db>, value: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Lit(YulLitKind::String(format!( + "\"{}\"", + value.replace('\\', "\\\\").replace('"', "\\\"") + ))), + } +} + +fn yul_call<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, + args: Vec>, +) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Call { + name: yul_ident(db, span, name), + args, + }, + } +} + +fn yul_let<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, + init: Option>, +) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Let { + names: vec![yul_ident(db, span, name)], + init, + }, + } +} + +fn yul_assign<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, + value: YulExpr<'db>, +) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Assign { + names: vec![yul_ident(db, span, name)], + value, + }, + } +} + +fn yul_expr_stmt<'db>(span: Span<'db>, expr: YulExpr<'db>) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Expr(expr), + } +} + +#[salsa::tracked] +fn prepare_contract_dispatch<'db>( + db: &'db dyn Db, + module_def: DefId<'db>, + contract: ContractDef<'db>, +) -> Option> { + let mut methods = Vec::new(); + let mut fallback = None; + for item in contract.items(db) { + let ContractItem::FunctionDef(function) = *item else { + continue; + }; + let sig = function.sig(db); + match function.kind(db) { + FuncKind::Function + if sig.is_abi_visible() && ident_text(db, &sig.name) != "fallback" => + { + methods.push(RawMethod { + def: function.def_id_value(db), + name: ident_text(db, &sig.name), + span: sig.span, + payable: sig.is_payable(), + params: explicit_param_types(sig.params.atom())?, + ret: sig.ret.unwrap_or_else(|| unit_ty(db, sig.span)), + }); + } + FuncKind::Fallback => { + fallback = Some(RawFallback { + name: ident_text(db, &sig.name), + payable: sig.is_payable(), + params: explicit_param_types(sig.params.atom())?, + ret: sig.ret.unwrap_or_else(|| unit_ty(db, sig.span)), + }); + } + FuncKind::Function | FuncKind::Constructor => {} + } + } + + let contract_def = contract.def_id_value(db); + let contract_name = ident_text(db, &contract.name_elem(db)); + let mut top_level_items = Vec::new(); + let mut origins = Vec::new(); + let mut declared_names = BTreeSet::new(); + for method in &methods { + if !declared_names.insert(method.name.clone()) { + continue; + } + let (adt, instance, mut generated) = + dispatch_name_declarations(db, module_def, contract_def, &contract_name, method); + top_level_items.push(Item::AdtDef(adt)); + top_level_items.push(Item::InstanceDef(instance)); + origins.append(&mut generated); + } + + let main = generated_dispatch_main(db, contract, &contract_name, &methods, fallback.as_ref())?; + let main_def = main.def_id_value(db); + origins.push(GeneratedOrigin { + def: main_def, + kind: GeneratedOriginKind::ContractDispatchMain, + contract: contract_def, + method: None, + span: contract.name_elem(db).span(db), + }); + if let Some(body) = main.body(db) { + origins.push(GeneratedOrigin { + def: body.def_id(db), + kind: GeneratedOriginKind::ContractDispatchMain, + contract: contract_def, + method: None, + span: contract.name_elem(db).span(db), + }); + } + + let mut contract_items = contract.items(db).clone(); + contract_items.push(ContractItem::FunctionDef(main)); + let prepared_contract = ContractDef::new( + db, + contract_def, + contract.span(db), + contract.leading_comments(db).clone(), + contract.kind(db), + contract.name_elem(db), + contract.ty_param_elems(db).clone(), + contract.fields(db).clone(), + contract.field_comments(db).clone(), + contract_items, + ); + Some(PreparedContractArtifacts { + contract: prepared_contract, + top_level_items, + origins, + }) +} + +fn explicit_param_types<'db>(params: &[FuncParam<'db>]) -> Option>> { + params + .iter() + .map(|param| match param { + FuncParam::Typed { ty, .. } => Some(*ty), + FuncParam::Untyped { .. } | FuncParam::Error { .. } => None, + }) + .collect() +} + +fn dispatch_name_declarations<'db>( + db: &'db dyn Db, + module_def: DefId<'db>, + contract: DefId<'db>, + contract_name: &str, + method: &RawMethod<'db>, +) -> (AdtDef<'db>, InstanceDef<'db>, Vec>) { + let ty_name = format!("DispatchNameTy_{contract_name}_{}", method.name); + let adt_def = generated_def( + db, + module_def, + DefKind::Adt, + &ty_name, + "solcore.generated.std_dispatch.name_type", + ); + let adt = AdtDef::new( + db, + adt_def, + method.span, + Vec::new(), + spanned_ident(db, method.span, &ty_name), + Vec::new(), + Vec::new(), + Vec::new(), + ); + + let instance_def = generated_def( + db, + module_def, + DefKind::Instance, + "SigString", + &format!("solcore.generated.std_dispatch.sig_string.{ty_name}"), + ); + let method_def = generated_def( + db, + instance_def, + DefKind::Function, + "sigStr", + "solcore.generated.std_dispatch.sig_string.method", + ); + let body_def = generated_def( + db, + method_def, + DefKind::FuncBody, + "sigStr", + "solcore.generated.std_dispatch.sig_string.method.body", + ); + let sig_string_method = sig_string_method( + db, + method_def, + body_def, + method.span, + &ty_name, + &method.name, + ); + let head = PredRef::new( + db, + PredRefKind { + ty: named_ty(db, method.span, &ty_name, Vec::new()), + class: spanned_ident(db, method.span, "SigString"), + args: SpannedElem::new(Vec::new(), method.span), + }, + ); + let instance = InstanceDef::new( + db, + instance_def, + method.span, + Vec::new(), + Vec::new(), + Vec::new(), + None, + head, + vec![sig_string_method], + ); + let origins = vec![ + GeneratedOrigin { + def: adt_def, + kind: GeneratedOriginKind::ContractDispatchNameType, + contract, + method: Some(method.def), + span: method.span, + }, + GeneratedOrigin { + def: instance_def, + kind: GeneratedOriginKind::ContractDispatchSigStringInstance, + contract, + method: Some(method.def), + span: method.span, + }, + GeneratedOrigin { + def: method_def, + kind: GeneratedOriginKind::ContractDispatchSigStringMethod, + contract, + method: Some(method.def), + span: method.span, + }, + GeneratedOrigin { + def: body_def, + kind: GeneratedOriginKind::ContractDispatchSigStringMethod, + contract, + method: Some(method.def), + span: method.span, + }, + ]; + (adt, instance, origins) +} + +fn sig_string_method<'db>( + db: &'db dyn Db, + method_def: DefId<'db>, + body_def: DefId<'db>, + span: Span<'db>, + ty_name: &str, + method_name: &str, +) -> FunctionDef<'db> { + let mut exprs = Arena::new(); + let value = exprs.alloc(Expr { + span, + kind: ExprKind::Lit(LitKind::String(format!("\"{method_name}\""))), + }); + let mut stmts = Arena::new(); + let ret = stmts.alloc(Stmt { + span, + kind: StmtKind::Return(Some(value)), + }); + let body = FuncBody::new(db, body_def, span, vec![ret], stmts, exprs, Arena::new()); + let proxy_ty = named_ty( + db, + span, + "Proxy", + vec![named_ty(db, span, ty_name, Vec::new())], + ); + let sig = FuncSig { + span, + type_vars: Vec::new(), + preds: Vec::new(), + visibility: None, + mutability: None, + name: spanned_ident(db, span, "sigStr"), + params: SpannedElem::new( + vec![FuncParam::Typed { + comptime: None, + name: spanned_ident(db, span, "p"), + ty: proxy_ty, + }], + span, + ), + ret: Some(named_ty(db, span, "string", Vec::new())), + ret_names: Vec::new(), + }; + FunctionDef::new( + db, + method_def, + span, + FuncKind::Function, + Vec::new(), + sig, + Some(body), + ) +} + +fn generated_dispatch_main<'db>( + db: &'db dyn Db, + contract: ContractDef<'db>, + contract_name: &str, + methods: &[RawMethod<'db>], + fallback: Option<&RawFallback<'db>>, +) -> Option> { + let span = contract.name_elem(db).span(db); + let mut builder = BodyBuilder::new(db, span); + let mut method_values = Vec::with_capacity(methods.len()); + for method in methods { + let name_ty = format!("DispatchNameTy_{contract_name}_{}", method.name); + let name = builder.proxy(named_ty(db, method.span, &name_ty, Vec::new())); + let payability = builder.proxy(named_ty( + db, + method.span, + if method.payable { + "Payable" + } else { + "NonPayable" + }, + Vec::new(), + )); + let args_ty = product_ty(db, method.span, &method.params); + let args = builder.proxy(args_ty); + let rets = builder.proxy(method.ret); + // A bare function value shares the contract value namespace with + // fields. Use an unspellable HIR-only reference so a method such as + // `allowance` cannot resolve to the field, nor can `C.Foo` resolve to a + // same-named constructor of a contract-local ADT. + let implementation = builder.ident(&compiler_contract_method_ref(&method.name)); + method_values.push(builder.call_path( + &["Method", "Method"], + vec![name, payability, args, rets, implementation], + )); + } + let methods = builder.product_expr(&method_values); + let fallback = match fallback { + Some(fallback) => { + let payability = builder.proxy(named_ty( + db, + span, + if fallback.payable { + "Payable" + } else { + "NonPayable" + }, + Vec::new(), + )); + let args_ty = product_ty(db, span, &fallback.params); + let args = builder.proxy(args_ty); + let rets = builder.proxy(fallback.ret); + let implementation = builder.ident(&compiler_contract_method_ref(&fallback.name)); + builder.call_path( + &["Fallback", "Fallback"], + vec![payability, args, rets, implementation], + ) + } + None => { + let payability = builder.proxy(named_ty(db, span, "NonPayable", Vec::new())); + let args = builder.proxy(unit_ty(db, span)); + let rets = builder.proxy(unit_ty(db, span)); + let implementation = builder.ident("fallback_default_implementation"); + builder.call_path( + &["Fallback", "Fallback"], + vec![payability, args, rets, implementation], + ) + } + }; + let contract_value = builder.call_path(&["Contract", "Contract"], vec![methods, fallback]); + let run = builder.call_path(&["RunContract", "exec"], vec![contract_value]); + let run_stmt = builder.alloc_stmt(StmtKind::Expr(run)); + let unit = builder.alloc_expr(ExprKind::Tuple(Vec::new())); + let return_stmt = builder.alloc_stmt(StmtKind::Return(Some(unit))); + + let contract_def = contract.def_id_value(db); + let function_def = generated_def( + db, + contract_def, + DefKind::Function, + GENERATED_MAIN_NAME, + MAIN_FINGERPRINT, + ); + let body_def = generated_def( + db, + function_def, + DefKind::FuncBody, + GENERATED_MAIN_NAME, + MAIN_BODY_FINGERPRINT, + ); + let (stmts, exprs, pats) = builder.finish(); + let body = FuncBody::new( + db, + body_def, + span, + vec![run_stmt, return_stmt], + stmts, + exprs, + pats, + ); + let sig = FuncSig { + span, + type_vars: Vec::new(), + preds: Vec::new(), + visibility: None, + mutability: None, + name: spanned_ident(db, span, GENERATED_MAIN_NAME), + params: SpannedElem::new(Vec::new(), span), + ret: Some(unit_ty(db, span)), + ret_names: Vec::new(), + }; + Some(FunctionDef::new( + db, + function_def, + span, + FuncKind::Function, + Vec::new(), + sig, + Some(body), + )) +} + +struct BodyBuilder<'db> { + db: &'db dyn Db, + span: Span<'db>, + stmts: Arena>, + exprs: Arena>, + pats: Arena>, +} + +impl<'db> BodyBuilder<'db> { + fn new(db: &'db dyn Db, span: Span<'db>) -> Self { + Self { + db, + span, + stmts: Arena::new(), + exprs: Arena::new(), + pats: Arena::new(), + } + } + + fn finish(self) -> (Arena>, Arena>, Arena>) { + (self.stmts, self.exprs, self.pats) + } + + fn alloc_stmt(&mut self, kind: StmtKind<'db>) -> Id> { + self.stmts.alloc(Stmt { + span: self.span, + kind, + }) + } + + fn alloc_expr(&mut self, kind: ExprKind<'db>) -> Id> { + self.exprs.alloc(Expr { + span: self.span, + kind, + }) + } + + fn alloc_pat(&mut self, kind: PatKind<'db>) -> Id> { + self.pats.alloc(Pat { + span: self.span, + kind, + }) + } + + fn ident(&mut self, name: &str) -> Id> { + let ident = spanned_ident(self.db, self.span, name); + self.alloc_expr(ExprKind::Ident(ident)) + } + + fn path(&mut self, segments: &[&str]) -> Id> { + let (first, rest) = segments + .split_first() + .expect("compiler-generated paths are non-empty"); + let mut expr = self.ident(first); + for segment in rest { + expr = self.alloc_expr(ExprKind::Field { + base: expr, + field: spanned_ident(self.db, self.span, segment), + }); + } + expr + } + + fn call(&mut self, callee: Id>, args: Vec>>) -> Id> { + self.alloc_expr(ExprKind::Call { callee, args }) + } + + fn call_ident(&mut self, name: &str, args: Vec>>) -> Id> { + let callee = self.ident(name); + self.call(callee, args) + } + + fn call_path(&mut self, path: &[&str], args: Vec>>) -> Id> { + let callee = self.path(path); + self.call(callee, args) + } + + fn let_stmt( + &mut self, + name: &str, + ty: Option>, + init: Option>>, + ) -> Id> { + self.alloc_stmt(StmtKind::Let { + comptime: None, + name: spanned_ident(self.db, self.span, name), + ty, + init, + }) + } + + fn proxy(&mut self, ty: TypeRef<'db>) -> Id> { + let proxy = self.path(&["Proxy", "Proxy"]); + let proxy_ty = named_ty(self.db, self.span, "Proxy", vec![ty]); + self.alloc_expr(ExprKind::TypeAscription { + expr: proxy, + ty: proxy_ty, + }) + } + + fn product_expr(&mut self, elems: &[Id>]) -> Id> { + match elems { + [] => self.alloc_expr(ExprKind::Tuple(Vec::new())), + [one] => *one, + [head, tail @ ..] => { + let tail = self.product_expr(tail); + self.alloc_expr(ExprKind::Tuple(vec![*head, tail])) + } + } + } + + fn product_pat(&mut self, names: &[String]) -> Id> { + match names { + [] => self.alloc_pat(PatKind::Tuple { elems: Vec::new() }), + [one] => { + let name = spanned_ident(self.db, self.span, one); + self.alloc_pat(PatKind::Var(name)) + } + names => { + let elems = names + .iter() + .map(|name| { + let name = spanned_ident(self.db, self.span, name); + self.alloc_pat(PatKind::Var(name)) + }) + .collect(); + self.alloc_pat(PatKind::Tuple { elems }) + } + } + } +} + +fn generated_def<'db>( + db: &'db dyn Db, + owner: DefId<'db>, + kind: DefKind, + name: &str, + fingerprint: &str, +) -> DefId<'db> { + DefId::new( + db, + owner.file(db), + Some(owner), + kind, + Some(name.to_owned()), + Some(fingerprint.to_owned()), + Disambiguator::ZERO, + ) +} + +fn product_ty<'db>(db: &'db dyn Db, span: Span<'db>, elems: &[TypeRef<'db>]) -> TypeRef<'db> { + match elems { + [] => unit_ty(db, span), + [one] => *one, + [head, tail @ ..] => tuple_ty(db, span, vec![*head, product_ty(db, span, tail)]), + } +} + +fn unit_ty<'db>(db: &'db dyn Db, span: Span<'db>) -> TypeRef<'db> { + tuple_ty(db, span, Vec::new()) +} + +fn tuple_ty<'db>(db: &'db dyn Db, span: Span<'db>, elems: Vec>) -> TypeRef<'db> { + TypeRef::new( + db, + TypeRefKind::Tuple { + elems: SpannedElem::new(elems, span), + }, + ) +} + +fn named_ty<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, + args: Vec>, +) -> TypeRef<'db> { + TypeRef::new( + db, + TypeRefKind::Named { + qualifier: None, + name: spanned_ident(db, span, name), + args: SpannedElem::new(args, span), + }, + ) +} + +fn spanned_ident<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, +) -> SpannedElem<'db, Ident<'db>> { + SpannedElem::new(Ident::new(db, name.to_owned()), span) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + sync::{Arc, Mutex}, + }; + + use hir::{ + anchor::DefLocationTable, + ast::item::{ContractItem, Item}, + input::SourceFile, + }; + use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, + }; + use parser::parse_file_to_hir; + use rustc_hash::FxHashMap; + use salsa::Setter; + + use super::*; + + #[salsa::db] + #[derive(Clone)] + struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, + executed: Arc>>, + } + + impl Default for TestDb { + fn default() -> Self { + let executed = Arc::new(Mutex::new(Vec::new())); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + }))), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: FxHashMap::default(), + executed, + } + } + } + + impl TestDb { + fn take_executed(&self) -> Vec { + std::mem::take(&mut *self.executed.lock().expect("execution log lock")) + } + + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } + } + + #[salsa::db] + impl salsa::Database for TestDb {} + + #[salsa::db] + impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } + } + + #[salsa::db] + impl parser::Db for TestDb {} + + #[salsa::db] + impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.expect("module tree") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot.expect("filesystem snapshot") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot.expect("module file snapshot") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } + } + + #[salsa::db] + impl crate::Db for TestDb {} + + fn db_with_main(src: &str) -> (TestDb, SourceFile) { + let mut db = TestDb::default(); + let main_root = PathBuf::from("/main"); + let std_root = PathBuf::from("/std"); + db.module_tree = Some(ModuleTree::new( + &db, + main_root.clone(), + std_root.clone(), + BTreeMap::new(), + )); + let main_path = main_root.join("main.solc"); + let std_path = std_root.join("std.solc"); + let dispatch_path = std_root.join("dispatch.solc"); + db.module_fs_snapshot = Some(ModuleFsSnapshot::new( + &db, + BTreeSet::from([main_path.clone(), std_path.clone(), dispatch_path.clone()]), + BTreeMap::from([ + (main_root, vec!["main".to_owned()]), + (std_root, vec!["std".to_owned(), "dispatch".to_owned()]), + ]), + )); + let main_file = SourceFile::new( + &db, + url::Url::from_file_path(&main_path).expect("main URL"), + Some(src.to_owned()), + ); + let dispatch_file = SourceFile::new( + &db, + url::Url::from_file_path(&dispatch_path).expect("dispatch URL"), + Some(String::new()), + ); + let std_file = SourceFile::new( + &db, + url::Url::from_file_path(&std_path).expect("std URL"), + Some(String::new()), + ); + db.insert_module_file( + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["std".to_owned()], + }, + std_file, + ); + db.insert_module_file( + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }, + main_file, + ); + db.insert_module_file( + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["dispatch".to_owned()], + }, + dispatch_file, + ); + let _ = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }, + ); + (db, main_file) + } + + fn source_module<'db>(db: &'db TestDb, file: SourceFile) -> Module<'db> { + parse_file_to_hir(db, file).module(db) + } + + fn first_contract<'db>(db: &'db TestDb, module: Module<'db>) -> ContractDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract") + } + + #[test] + fn preserves_source_and_builds_effective_dispatch_overlay() { + let src = r#" +import std.dispatch; +contract C { function answer(x: uint256) public returns (uint256) { return x; } } +"#; + let (db, file) = db_with_main(src); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + assert_eq!(prepared.source(&db), source); + assert_ne!(prepared.module(&db), source); + assert_eq!(file.content(&db).as_deref(), Some(src)); + + let contract = first_contract(&db, prepared.module(&db)); + let main = prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .expect("generated runtime main"); + assert!(matches!( + prepared.origin_for_def(&db, main).map(|origin| origin.kind), + Some(GeneratedOriginKind::ContractDispatchMain) + )); + assert!(contract.items(&db).iter().any(|item| matches!( + item, + ContractItem::FunctionDef(function) + if function.def_id_value(&db) == main + && ident_text(&db, &function.sig(&db).name) == GENERATED_MAIN_NAME + ))); + assert_eq!( + prepare_module(&db, prepared.module(&db)).module(&db), + prepared.module(&db) + ); + } + + #[test] + fn preparation_preserves_contract_and_field_comments() { + let src = r#" +import std; +import std.dispatch; +// contract documentation +contract C { + // stored value documentation + stored: word; + // constructor documentation + constructor() {} + // method documentation + function answer(x: uint256) public returns (uint256) { return x; } +} +"#; + let (db, file) = db_with_main(src); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + let source_contract = first_contract(&db, prepared.source(&db)); + let effective_contract = first_contract(&db, prepared.module(&db)); + + assert_eq!( + trimmed_comment_texts(source_contract.leading_comments(&db)), + ["contract documentation"] + ); + assert_eq!( + effective_contract.leading_comments(&db), + source_contract.leading_comments(&db) + ); + assert_eq!( + effective_contract.field_comments(&db), + source_contract.field_comments(&db) + ); + + let fields = effective_contract + .fields_with_comments(&db) + .collect::>(); + assert_eq!(fields.len(), 1); + assert_eq!( + trimmed_comment_texts(fields[0].1), + ["stored value documentation"] + ); + + let mut generated_functions = 0; + for item in effective_contract.items(&db) { + let ContractItem::FunctionDef(function) = item else { + continue; + }; + if prepared + .origin_for_def(&db, function.def_id_value(&db)) + .is_some() + { + generated_functions += 1; + assert!(function.leading_comments(&db).is_empty()); + } + } + assert!(generated_functions > 0); + + let mut generated_adts = 0; + let mut generated_instances = 0; + for item in prepared.module(&db).items(&db) { + match item { + Item::AdtDef(adt) + if prepared + .origin_for_def(&db, adt.def_id_value(&db)) + .is_some() => + { + generated_adts += 1; + assert!(adt.leading_comments(&db).is_empty()); + assert_eq!(adt.ctors(&db).len(), adt.ctor_comments(&db).len()); + assert!( + adt.ctors_with_comments(&db) + .all(|(_, comments)| comments.is_empty()) + ); + } + Item::InstanceDef(instance) + if prepared + .origin_for_def(&db, instance.def_id_value(&db)) + .is_some() => + { + generated_instances += 1; + assert!(instance.leading_comments(&db).is_empty()); + assert!( + instance + .methods(&db) + .iter() + .all(|method| method.leading_comments(&db).is_empty()) + ); + } + _ => {} + } + } + assert!(generated_adts > 0); + assert!(generated_instances > 0); + } + + fn trimmed_comment_texts(comments: &[hir::ast::SourceComment]) -> Vec<&str> { + comments.iter().map(|comment| comment.text.trim()).collect() + } + + #[test] + fn runtime_dispatch_is_implicit_and_existing_main_suppresses_it() { + let (db, file) = db_with_main( + "contract C { function answer() public returns (uint256) { return uint256.uint256(1); } }", + ); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + assert_ne!(prepared.module(&db), source); + let contract = first_contract(&db, prepared.module(&db)); + assert!( + prepared + .contract_deployment_main(&db, contract.def_id_value(&db)) + .is_some() + ); + assert!( + prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .is_some() + ); + + let (db, file) = db_with_main( + r#" +import std.dispatch; +contract C { function main() returns () {} } +"#, + ); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + assert_ne!(prepared.module(&db), source); + let contract = first_contract(&db, prepared.module(&db)); + assert!( + prepared + .contract_deployment_main(&db, contract.def_id_value(&db)) + .is_some() + ); + assert!( + prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .is_none() + ); + } + + #[test] + fn nonempty_constructor_is_prepared_without_injecting_imports() { + let (db, file) = + db_with_main("contract C { constructor(x: word) {} function main() returns () {} }"); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + assert_ne!(prepared.module(&db), source); + let contract = first_contract(&db, prepared.module(&db)); + assert!( + prepared + .contract_deployment_main(&db, contract.def_id_value(&db)) + .is_some() + ); + assert!(prepared.module(&db).items(&db).iter().all(|item| { + !matches!(item, Item::Import(import) if import.def_id(&db).fingerprint(&db).is_some()) + })); + } + + #[test] + fn constructor_overlay_preserves_source_and_generates_deployment_entry() { + let (db, file) = db_with_main( + r#" +import std; +import std.dispatch; +contract C { + constructor(x: word, y: word) payable { let z = x; } + function main() returns () { return (); } +} +"#, + ); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + let source_contract = first_contract(&db, prepared.source(&db)); + assert!(source_contract.items(&db).iter().any(|item| matches!( + item, + ContractItem::FunctionDef(function) + if function.kind(&db) == FuncKind::Constructor + ))); + + let effective = first_contract(&db, prepared.module(&db)); + assert!(!effective.items(&db).iter().any(|item| matches!( + item, + ContractItem::FunctionDef(function) + if function.kind(&db) == FuncKind::Constructor + ))); + let names = effective + .items(&db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) => { + Some(ident_text(&db, &function.sig(&db).name)) + } + _ => None, + }) + .collect::>(); + assert!(names.contains(CONSTRUCTOR_INIT_NAME), "{names:?}"); + assert!(names.contains(CONSTRUCTOR_COPY_NAME), "{names:?}"); + assert!(names.contains(DEPLOYMENT_MAIN_NAME), "{names:?}"); + let deployment = prepared + .contract_deployment_main(&db, effective.def_id_value(&db)) + .expect("generated deployment main"); + assert!(is_contract_deployment_main_def(&db, deployment)); + assert!(matches!( + prepared + .origin_for_def(&db, deployment) + .map(|origin| origin.kind), + Some(GeneratedOriginKind::ContractDeploymentMain) + )); + assert!( + prepared + .contract_dispatch_main(&db, effective.def_id_value(&db)) + .is_none() + ); + } + + #[test] + fn explicit_constructor_overlay_is_idempotent() { + let (db, file) = db_with_main( + r#" +import std; +contract C { + constructor(x: word) payable { let saved = x; } + function main() returns () { return (); } +} +"#, + ); + let source = source_module(&db, file); + let first = prepare_module(&db, source).module(&db); + let second = prepare_module(&db, first).module(&db); + assert_eq!(second, first); + + let contract = first_contract(&db, second); + let generated = contract + .items(&db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) => function.def_id_value(&db).fingerprint(&db), + _ => None, + }) + .collect::>(); + for fingerprint in [ + CONSTRUCTOR_INIT_FINGERPRINT, + CONSTRUCTOR_COPY_FINGERPRINT, + DEPLOYMENT_MAIN_FINGERPRINT, + ] { + assert_eq!( + generated + .iter() + .filter(|candidate| candidate.as_str() == fingerprint) + .count(), + 1, + "{generated:?}" + ); + } + } + + #[test] + fn constructor_body_edit_keeps_generated_wrapper_identity() { + let before = r#" +import std; +import std.dispatch; +contract C { + constructor(x: word) { let z = 1; } + function main() returns () { return (); } +} +"#; + let after = r#" +import std; +import std.dispatch; +contract C { + constructor(x: word) { let z = 2; } + function main() returns () { return (); } +} +"#; + let (mut db, file) = db_with_main(before); + let before_module = prepare_module(&db, source_module(&db, file)).module(&db); + let before_contract = first_contract(&db, before_module); + let before_defs = constructor_wrapper_defs(&db, before_contract); + + file.set_content(&mut db).to(Some(after.to_owned())); + let after_module = prepare_module(&db, source_module(&db, file)).module(&db); + let after_contract = first_contract(&db, after_module); + let after_defs = constructor_wrapper_defs(&db, after_contract); + assert_eq!(before_defs, after_defs); + } + + fn constructor_wrapper_defs( + db: &TestDb, + contract: ContractDef<'_>, + ) -> BTreeMap { + contract + .items(db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) => { + let name = ident_text(db, &function.sig(db).name); + [ + CONSTRUCTOR_INIT_NAME, + CONSTRUCTOR_COPY_NAME, + DEPLOYMENT_MAIN_NAME, + ] + .contains(&name.as_str()) + .then(|| (name, format!("{:?}", function.def_id_value(db)))) + } + _ => None, + }) + .collect() + } + + #[test] + fn deduplicates_overloaded_method_name_declarations() { + let (db, file) = db_with_main( + r#" +import std.dispatch; +contract C { + function get(x: uint256) public returns (uint256) { return x; } + function get(x: bool) public returns (bool) { return x; } +} +"#, + ); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + let generated_name_types = prepared + .module(&db) + .items(&db) + .iter() + .filter(|item| matches!(item, Item::AdtDef(adt) if ident_text(&db, &adt.name_elem(&db)) == "DispatchNameTy_C_get")) + .count(); + assert_eq!(generated_name_types, 1); + } + + #[test] + fn omitted_return_uses_unit_and_body_edit_keeps_generated_identity() { + let before = r#" +import std.dispatch; +contract C { function ping() public { let x = 1; } } +"#; + let after = r#" +import std.dispatch; +contract C { function ping() public { let x = 2; } } +"#; + let (mut db, file) = db_with_main(before); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + let contract = first_contract(&db, prepared.module(&db)); + let before_main = prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .expect("generated main"); + let generated_main = contract + .items(&db) + .iter() + .find_map(|item| match item { + ContractItem::FunctionDef(function) + if function.def_id_value(&db) == before_main => + { + Some(*function) + } + _ => None, + }) + .expect("generated main function"); + let body = generated_main.body(&db).expect("generated main body"); + let unit_proxy_count = body + .exprs(&db) + .iter() + .filter(|(_, expr)| { + let ExprKind::TypeAscription { ty, .. } = expr.kind else { + return false; + }; + let TypeRefKind::Named { name, args, .. } = ty.kind(&db) else { + return false; + }; + if ident_text(&db, name) != "Proxy" { + return false; + } + matches!( + args.atom().as_slice(), + [arg] + if matches!( + arg.kind(&db), + TypeRefKind::Tuple { elems } if elems.atom().is_empty() + ) + ) + }) + .count(); + // ping's empty argument product and omitted return, plus the default + // fallback's empty argument and return products. + assert_eq!(unit_proxy_count, 4); + let before_main = format!("{before_main:?}"); + let _ = db.take_executed(); + + file.set_content(&mut db).to(Some(after.to_owned())); + let source = source_module(&db, file); + let prepared = prepare_module(&db, source); + let contract = first_contract(&db, prepared.module(&db)); + let after_main = prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .expect("generated main"); + assert_eq!(before_main, format!("{after_main:?}")); + let executed = db.take_executed(); + assert!( + executed + .iter() + .any(|event| event.contains("parse_file_to_hir")), + "body edit must exercise a new parse revision: {executed:#?}" + ); + assert_eq!( + executed + .iter() + .filter(|event| event.contains("prepare_module")) + .count(), + 0, + "body-only edits must backdate module preparation: {executed:#?}" + ); + assert_eq!( + executed + .iter() + .filter(|event| event.contains("prepare_contract_dispatch")) + .count(), + 0, + "body-only edits must backdate the generated contract overlay: {executed:#?}" + ); + } +} diff --git a/crates/hir-ty/src/solver/canonical.rs b/crates/hir-ty/src/solver/canonical.rs new file mode 100644 index 00000000..ecc0e11c --- /dev/null +++ b/crates/hir-ty/src/solver/canonical.rs @@ -0,0 +1,464 @@ +use super::*; + +/// One rigid variable carried from the top-level solver context. +/// +/// `origin` is stable for the lifetime of one tabled-engine run, while +/// `actual` is that variable's id in the current goal coordinate system. +/// Keeping both values is essential once nested goals have been +/// canonicalized: their `actual` ids shift around flex variables, but local +/// givens are still expressed in the original coordinate system. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct RigidVar { + origin: u32, + actual: u32, +} + +impl RigidVar { + pub(super) fn identity(var: u32) -> Self { + Self { + origin: var, + actual: var, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct TableKey<'db> { + /// Goal predicate with flex variables renamed to `0..flex_count`. + pub(super) pred: Pred<'db>, + /// Number of solvable (flex) variables in `pred`. + pub(super) flex_count: u32, + /// Stable origins and canonical ids of the rigid context variables. + /// + /// This mapping participates in equality and hashing. Equal predicate + /// shapes whose rigid variables originate from different local givens + /// must not share a table entry. + rigid_vars: Vec, +} + +impl<'db> TableKey<'db> { + pub(super) fn allowed_vars(&self) -> FxHashSet { + (0..self.flex_count).collect() + } + + pub(super) fn canonical_context_vars(&self) -> FxHashSet { + self.rigid_vars.iter().map(|var| var.actual).collect() + } + + pub(super) fn rigid_vars(&self) -> &[RigidVar] { + &self.rigid_vars + } +} + +#[derive(Clone, Default)] +pub(super) struct GoalRenaming { + flex_actuals: Vec, + /// Canonical rigid id -> caller rigid id. + rigid_actuals: FxHashMap, + fresh_base: u32, +} + +impl GoalRenaming { + fn flex_count(&self) -> u32 { + self.flex_actuals.len() as u32 + } + + fn actual_var(&self, key_var: u32) -> u32 { + if key_var < self.flex_count() { + self.flex_actuals[key_var as usize] + } else { + self.rigid_actuals.get(&key_var).copied().unwrap_or(key_var) + } + } + + fn is_context_var(&self, key_var: u32) -> bool { + key_var < self.flex_count() || self.rigid_actuals.contains_key(&key_var) + } +} + +/// Compute a goal's canonical tabling `TableKey` together with the +/// `GoalRenaming` that maps the key's canonical variables back to the caller's. +/// +/// Solvable variables in `allowed_vars` are renumbered to `0..flex_count` so +/// that goals equal up to renaming share one table entry; `rigid_vars` tracks +/// fixed variables by stable origin so nested goals can map local givens into +/// the same coordinate system. +pub(super) fn canonicalize_goal<'db>( + db: &'db dyn Db, + pred: Pred<'db>, + allowed_vars: &FxHashSet, + rigid_vars: &[RigidVar], +) -> (TableKey<'db>, GoalRenaming) { + let pred_vars_in_order = pred_vars_in_order(db, pred); + let pred_vars = pred_vars_in_order.iter().copied().collect::>(); + + // A caller passes rigid variables in its own coordinate system. Extend + // that mapping for any fixed variable first encountered in this goal, + // then sort by stable origin. In normal solver use the extension only + // happens for top-level rigid variables that do not occur in a local + // given; clause-local variables are all present in `allowed_vars`. + let mut caller_rigid_vars = rigid_vars.to_vec(); + let known_rigid_actuals = caller_rigid_vars + .iter() + .map(|var| var.actual) + .collect::>(); + let flex_actuals = pred_vars_in_order + .iter() + .copied() + .filter(|var| allowed_vars.contains(var) && !known_rigid_actuals.contains(var)) + .collect::>(); + + let flex_actual_set = flex_actuals.iter().copied().collect::>(); + let mut new_rigid_actuals = pred_vars + .iter() + .copied() + .filter(|var| !flex_actual_set.contains(var) && !known_rigid_actuals.contains(var)) + .collect::>(); + new_rigid_actuals.sort_unstable(); + let mut used_origins = caller_rigid_vars + .iter() + .map(|var| var.origin) + .collect::>(); + let mut next_origin = used_origins + .iter() + .copied() + .chain(pred_vars.iter().copied()) + .max() + .map_or(0, |var| var + 1); + for actual in new_rigid_actuals { + let origin = if used_origins.insert(actual) { + actual + } else { + while !used_origins.insert(next_origin) { + next_origin += 1; + } + let origin = next_origin; + next_origin += 1; + origin + }; + caller_rigid_vars.push(RigidVar { origin, actual }); + } + caller_rigid_vars.sort_unstable_by_key(|var| var.origin); + + // Canonical coordinates are dense and independent of the caller's ids: + // flex variables occupy `0..flex_count`, followed by rigid variables in + // stable-origin order. This is what makes alpha-equivalent nested goals + // hash to the same `TableKey` instead of drifting on every recursion. + let flex_count = flex_actuals.len() as u32; + let mut var_map = flex_actuals + .iter() + .enumerate() + .map(|(index, actual)| (*actual, index as u32)) + .collect::>(); + var_map.extend( + caller_rigid_vars + .iter() + .enumerate() + .map(|(rank, var)| (var.actual, flex_count + rank as u32)), + ); + let canonicalizer = GoalCanonicalizer { + db, + flex_count, + var_map, + }; + let canonical_pred = canonicalizer.pred(pred); + let canonical_rigid_vars = caller_rigid_vars + .iter() + .enumerate() + .map(|(rank, var)| RigidVar { + origin: var.origin, + actual: flex_count + rank as u32, + }) + .collect::>(); + let rigid_actuals = canonical_rigid_vars + .iter() + .zip(&caller_rigid_vars) + .map(|(canonical, caller)| (canonical.actual, caller.actual)) + .collect(); + let fresh_base = caller_rigid_vars + .iter() + .map(|var| var.actual) + .chain(allowed_vars.iter().copied()) + .chain(pred_vars.iter().copied()) + .max() + .map_or(0, |var| var + 1); + ( + TableKey { + pred: canonical_pred, + flex_count, + rigid_vars: canonical_rigid_vars, + }, + GoalRenaming { + flex_actuals, + rigid_actuals, + fresh_base, + }, + ) +} + +/// Collect variables in structural first-occurrence order. Numeric variable +/// ids are caller-local, so sorting them would give alpha-equivalent goals +/// different canonical predicates when two callers allocate their fresh +/// variables in a different order. +fn pred_vars_in_order<'db>(db: &'db dyn Db, pred: Pred<'db>) -> Vec { + let mut vars = Vec::new(); + let mut seen = FxHashSet::default(); + match pred.kind(db) { + PredKind::InClass { main, args, .. } => { + collect_ty_vars_in_order(db, *main, &mut vars, &mut seen); + for arg in args { + collect_ty_vars_in_order(db, *arg, &mut vars, &mut seen); + } + } + PredKind::Eq { lhs, rhs } => { + collect_ty_vars_in_order(db, *lhs, &mut vars, &mut seen); + collect_ty_vars_in_order(db, *rhs, &mut vars, &mut seen); + } + PredKind::Error => {} + } + vars +} + +fn collect_ty_vars_in_order<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + vars: &mut Vec, + seen: &mut FxHashSet, +) { + match ty.kind(db) { + TyKind::BoundVar(var) => { + if seen.insert(var.index) { + vars.push(var.index); + } + } + TyKind::Named { args, .. } => { + for arg in args { + collect_ty_vars_in_order(db, *arg, vars, seen); + } + } + TyKind::Function { params, ret } => { + for param in params { + collect_ty_vars_in_order(db, *param, vars, seen); + } + collect_ty_vars_in_order(db, *ret, vars, seen); + } + TyKind::Tuple(elems) => { + for elem in elems { + collect_ty_vars_in_order(db, *elem, vars, seen); + } + } + TyKind::Comptime(inner) => collect_ty_vars_in_order(db, *inner, vars, seen), + TyKind::Error | TyKind::Unknown => {} + } +} + +struct GoalCanonicalizer<'db> { + db: &'db dyn Db, + flex_count: u32, + var_map: FxHashMap, +} + +impl<'db> GoalCanonicalizer<'db> { + fn var(&self, var: u32) -> u32 { + self.var_map + .get(&var) + .copied() + .unwrap_or(self.flex_count + var) + } + + fn pred(&self, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(self.db) { + PredKind::InClass { class, main, args } => Pred::in_class( + self.db, + *class, + self.ty(*main), + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => Pred::eq(self.db, self.ty(*lhs), self.ty(*rhs)), + PredKind::Error => Pred::error(self.db), + } + } + + fn ty(&self, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(self.db) { + TyKind::BoundVar(var) => Ty::bound(self.db, self.var(var.index)), + TyKind::Named { ctor, args } => Ty::named( + self.db, + *ctor, + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + TyKind::Function { params, ret } => Ty::function( + self.db, + params.iter().map(|param| self.ty(*param)).collect(), + self.ty(*ret), + ), + TyKind::Tuple(elems) => { + Ty::tuple(self.db, elems.iter().map(|elem| self.ty(*elem)).collect()) + } + TyKind::Comptime(inner) => Ty::comptime(self.db, self.ty(*inner)), + TyKind::Error | TyKind::Unknown => ty, + } + } +} + +pub(super) fn canonicalize_local_given<'db>( + db: &'db dyn Db, + pred: Pred<'db>, + key: &TableKey<'db>, +) -> Pred<'db> { + // Local givens retain the stable origin ids from the inference context; + // map those origins directly to this particular subgoal's canonical rigid + // ids. Reusing the flex-variable map here loses the correlation after the + // first nested canonicalization. + let var_map = key + .rigid_vars + .iter() + .map(|var| (var.origin, var.actual)) + .collect::>(); + GoalCanonicalizer { + db, + flex_count: key.flex_count, + var_map, + } + .pred(pred) +} + +pub(super) fn actualize_answer<'db>( + db: &'db dyn Db, + answer: &Answer<'db>, + renaming: &GoalRenaming, +) -> Answer<'db> { + let actualizer = AnswerActualizer::new(db, answer, renaming); + let mut values = answer + .candidate + .subst + .values + .iter() + .filter_map(|(var, ty)| { + let var = renaming.actual_var(*var); + let ty = actualizer.ty(*ty); + (!matches!(ty.kind(db), TyKind::BoundVar(bound) if bound.index == var)) + .then_some((var, ty)) + }) + .collect::>(); + values.sort_unstable_by_key(|(var, _)| *var); + Answer { + candidate: Candidate { + subst: Substitution { values }, + evidence: actualizer.evidence(answer.candidate.evidence.clone()), + }, + origin: answer.origin.clone(), + } +} + +struct AnswerActualizer<'db, 'a> { + db: &'db dyn Db, + renaming: &'a GoalRenaming, + local_vars: FxHashMap, +} + +impl<'db, 'a> AnswerActualizer<'db, 'a> { + fn new(db: &'db dyn Db, answer: &Answer<'db>, renaming: &'a GoalRenaming) -> Self { + let mut vars = FxHashSet::default(); + for (_, ty) in &answer.candidate.subst.values { + collect_ty_vars(db, *ty, &mut vars); + } + collect_evidence_vars(db, &answer.candidate.evidence, &mut vars); + + let mut local_vars = vars + .into_iter() + .filter(|var| !renaming.is_context_var(*var)) + .collect::>(); + local_vars.sort_unstable(); + let local_vars = local_vars + .into_iter() + .enumerate() + .map(|(index, var)| (var, renaming.fresh_base + index as u32)) + .collect(); + + Self { + db, + renaming, + local_vars, + } + } + + fn var(&self, var: u32) -> u32 { + if let Some(actual) = self.local_vars.get(&var) { + *actual + } else { + self.renaming.actual_var(var) + } + } + + fn pred(&self, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(self.db) { + PredKind::InClass { class, main, args } => Pred::in_class( + self.db, + *class, + self.ty(*main), + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => Pred::eq(self.db, self.ty(*lhs), self.ty(*rhs)), + PredKind::Error => Pred::error(self.db), + } + } + + fn ty(&self, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(self.db) { + TyKind::BoundVar(var) => Ty::bound(self.db, self.var(var.index)), + TyKind::Named { ctor, args } => Ty::named( + self.db, + *ctor, + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + TyKind::Function { params, ret } => Ty::function( + self.db, + params.iter().map(|param| self.ty(*param)).collect(), + self.ty(*ret), + ), + TyKind::Tuple(elems) => { + Ty::tuple(self.db, elems.iter().map(|elem| self.ty(*elem)).collect()) + } + TyKind::Comptime(inner) => Ty::comptime(self.db, self.ty(*inner)), + TyKind::Error | TyKind::Unknown => ty, + } + } + + fn evidence(&self, evidence: Evidence<'db>) -> Evidence<'db> { + match evidence { + Evidence::Instance { + instance, + args, + sub_evidence, + } => Evidence::Instance { + instance, + args: args.into_iter().map(|arg| self.ty(arg)).collect(), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| self.evidence(evidence)) + .collect(), + }, + Evidence::Builtin { pred } => Evidence::Builtin { + pred: self.pred(pred), + }, + Evidence::Superclass { class, pred, child } => Evidence::Superclass { + class, + pred: self.pred(pred), + child: Box::new(self.evidence(*child)), + }, + Evidence::Derived { + kind, + pred, + sub_evidence, + } => Evidence::Derived { + kind, + pred: self.pred(pred), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| self.evidence(evidence)) + .collect(), + }, + } + } +} diff --git a/crates/hir-ty/src/solver/derived_generic.rs b/crates/hir-ty/src/solver/derived_generic.rs new file mode 100644 index 00000000..add55d0a --- /dev/null +++ b/crates/hir-ty/src/solver/derived_generic.rs @@ -0,0 +1,411 @@ +use super::*; + +pub fn generic_derivation_diagnostics<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + env: &nameres::ModuleImportSurface<'db>, +) -> Vec { + let Some(generic) = visible_generic_class(db, env).or_else(|| local_generic_class(db, module)) + else { + return Vec::new(); + }; + let excluded = no_generic_instance_for(db, module); + let manual = manual_generic_instance_types(db, module, item_resolutions, generic); + local_adt_infos(db, module) + .into_iter() + .filter(|info| manual.contains(&info.adt.def_id_value(db))) + .filter(|info| !excluded.contains(&adt_name(db, info.adt))) + .map(|info| TypeckDiagnostic::GenericDeriveConflict { + span: LabelSpan::from_span(db, info.adt.name_elem(db).span(db)), + ty: adt_name(db, info.adt), + }) + .collect() +} + +#[derive(Clone)] +pub(super) struct AdtDeriveInfo<'db> { + pub(super) adt: AdtDef<'db>, + pub(super) type_vars: Vec>, +} + +pub(super) fn visible_generic_class<'db>( + db: &'db dyn Db, + env: &nameres::ModuleImportSurface<'db>, +) -> Option> { + env.types + .get("Generic") + .and_then(|resolution| generic_class_from_resolution(db, resolution)) + .or_else(|| { + env.item_scope + .as_ref() + .and_then(|scope| local_generic_class(db, scope.module)) + }) +} + +pub(super) fn imported_generic_class<'db>( + db: &'db dyn Db, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, +) -> Option> { + item_resolutions + .preds + .iter() + .find_map(|entry| generic_class_from_resolution(db, &entry.resolution)) + .or_else(|| { + item_resolutions + .types + .iter() + .find_map(|entry| generic_class_from_resolution(db, &entry.resolution)) + }) +} + +fn generic_class_from_resolution<'db>( + db: &'db dyn Db, + resolution: &hir_nameres::Resolution<'db>, +) -> Option> { + match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } if def.name(db).as_deref() == Some("Generic") => Some(*def), + _ => None, + } +} + +pub(super) fn local_generic_class<'db>(db: &'db dyn Db, module: Module<'db>) -> Option> { + module.items(db).iter().find_map(|item| { + let Item::ClassDef(class) = item else { + return None; + }; + let PredKind::InClass { + class: ClassId::User(def), + .. + } = TypeLowering::from_item_resolutions( + db, + &hir_nameres::resolve_item_type_facts(db, module), + BinderEnv::from_type_vars(&type_var_bindings( + class.def_id_value(db), + class.type_var_elems(db), + )), + ) + .lower_pred(class.head(db)) + .kind(db) + else { + return None; + }; + (def.name(db).as_deref() == Some("Generic")).then_some(*def) + }) +} + +pub(super) fn no_generic_instance_for<'db>( + db: &'db dyn HirDb, + module: Module<'db>, +) -> FxHashSet { + let mut excluded = FxHashSet::default(); + for item in module.items(db) { + let Item::Pragma(pragma) = item else { + continue; + }; + if (*pragma.name(db).atom()).text(db) != "noGenericInstanceFor" { + continue; + } + excluded.extend( + pragma + .items(db) + .iter() + .map(|item| (*item.atom()).text(db).to_owned()), + ); + } + excluded +} + +pub(super) fn manual_generic_instance_types<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + generic: DefId<'db>, +) -> FxHashSet> { + let mut types = FxHashSet::default(); + for item in module.items(db) { + let Item::InstanceDef(instance) = item else { + continue; + }; + let type_vars = type_var_bindings(instance.def_id_value(db), instance.type_var_elems(db)); + let lowerer = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let head = normalizer.normalize_pred(lowerer.lower_pred(instance.head(db))); + let PredKind::InClass { + class: ClassId::User(class), + main, + .. + } = head.kind(db) + else { + continue; + }; + if *class != generic { + continue; + } + if let Some(def) = ty_head_adt_def(db, *main) { + types.insert(def); + } + } + types +} + +fn ty_head_adt_def<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Option> { + match ty.kind(db) { + TyKind::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }), + .. + } => Some(*def), + _ => None, + } +} + +pub(super) fn local_adt_infos<'db>( + db: &'db dyn HirDb, + module: Module<'db>, +) -> Vec> { + let mut infos = Vec::new(); + for item in module.items(db) { + collect_local_adt_infos(db, *item, &[], &mut infos); + } + infos +} + +fn collect_local_adt_infos<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + infos: &mut Vec>, +) { + match item { + Item::AdtDef(adt) => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + adt.def_id_value(db), + adt.ty_param_elems(db), + )); + infos.push(AdtDeriveInfo { adt, type_vars }); + } + Item::ContractDef(contract) => { + let mut inherited = inherited.to_vec(); + inherited.extend(type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + for item in contract.items(db) { + if let ContractItem::AdtDef(adt) = *item { + collect_local_adt_infos(db, Item::AdtDef(adt), &inherited, infos); + } + } + } + _ => {} + } +} + +pub(super) fn adt_name<'db>(db: &'db dyn HirDb, adt: AdtDef<'db>) -> String { + ident_text(db, &adt.name_elem(db)) +} + +/// Returns the synthesized `Generic` instance plan for `adt` in `module`. +#[salsa::tracked] +pub fn derived_generic_plan<'db>( + db: &'db dyn Db, + module: Module<'db>, + adt: AdtDef<'db>, +) -> Option> { + let item_resolutions = resolve_derived_generic_item_types(db, module); + let info = local_adt_infos(db, module) + .into_iter() + .find(|info| info.adt.def_id_value(db) == adt.def_id_value(db))?; + if info.adt.ctors(db).is_empty() { + return None; + } + Some(derived_generic_plan_with_resolutions( + db, + module, + &item_resolutions, + &info, + )) +} + +/// Returns the synthesized `Generic` plan only when solver instance derivation +/// is eligible for `adt` and the selected `Generic` class. +/// +/// Unlike [`derived_generic_plan`], this query respects both +/// `pragma solcore noGenericInstanceFor` and an explicit impl for the same ADT. Callers +/// that manufacture solver evidence must use this eligibility-aware form. +#[salsa::tracked] +pub fn derived_generic_instance_plan<'db>( + db: &'db dyn Db, + module: Module<'db>, + adt: AdtDef<'db>, + generic: DefId<'db>, +) -> Option> { + let item_resolutions = resolve_derived_generic_item_types(db, module); + let info = local_adt_infos(db, module) + .into_iter() + .find(|info| info.adt.def_id_value(db) == adt.def_id_value(db))?; + derived_generic_instance_plan_with_resolutions(db, module, &item_resolutions, &info, generic) +} + +fn resolve_derived_generic_item_types<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> hir_nameres::ItemResolutionFacts<'db> { + let file = module.def_id_value(db).file(db); + let Some(module_id) = nameres::module_id_for_source_file(db, file) else { + return hir_nameres::resolve_item_type_facts(db, module); + }; + let env = nameres::module_import_surface(db, module_id); + let Some(item_scope) = env.item_scope.as_ref() else { + return hir_nameres::resolve_item_type_facts(db, module); + }; + hir_nameres::resolve_item_type_facts_with_imports(db, module, item_scope, &env) +} + +pub(super) fn derived_generic_instance_plan_with_resolutions<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + info: &AdtDeriveInfo<'db>, + generic: DefId<'db>, +) -> Option> { + if info.adt.ctors(db).is_empty() + || no_generic_instance_for(db, module).contains(&adt_name(db, info.adt)) + || manual_generic_instance_types(db, module, item_resolutions, generic) + .contains(&info.adt.def_id_value(db)) + { + return None; + } + Some(derived_generic_plan_with_resolutions( + db, + module, + item_resolutions, + info, + )) +} + +pub(super) fn derived_generic_plan_with_resolutions<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + info: &AdtDeriveInfo<'db>, +) -> DerivedGenericPlan<'db> { + let lowerer = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.type_vars), + ); + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let ctors = info.adt.ctors(db); + let total = ctors.len(); + let product_reps = ctors + .iter() + .map(|ctor| { + let fields = normalizer.normalize_ty(lowerer.lower_type(*ctor.fields.atom())); + constructor_rep_ty(db, fields) + }) + .collect::>(); + let from_arms = ctors + .iter() + .zip(product_reps.iter()) + .enumerate() + .map(|(index, (ctor, product_rep))| { + let (inr_depth, wraps_inl) = generic_sum_wrapping(index, total); + DerivedGenericFromArm { + ctor_index: index as u32, + field_count: ctor.field_count as u32, + ctor_name: ident_text(db, &ctor.name), + product_rep: *product_rep, + inr_depth, + wraps_inl, + } + }) + .collect(); + let to_arms = ctors + .iter() + .zip(product_reps.iter()) + .enumerate() + .map(|(index, (ctor, product_rep))| { + let (inr_depth, wraps_inl) = generic_sum_wrapping(index, total); + DerivedGenericToArm { + ctor_index: index as u32, + field_count: ctor.field_count as u32, + ctor_name: ident_text(db, &ctor.name), + product_rep: *product_rep, + inr_depth, + wraps_inl, + } + }) + .collect(); + DerivedGenericPlan { + adt: info.adt.def_id_value(db), + rep: sum_rep_ty(db, product_reps), + from_arms, + to_arms, + } +} + +fn generic_sum_wrapping(index: usize, total: usize) -> (u32, bool) { + if total <= 1 { + return (0, false); + } + if index + 1 == total { + ((total - 1) as u32, false) + } else { + (index as u32, true) + } +} + +fn constructor_rep_ty<'db>(db: &'db dyn Db, fields: Ty<'db>) -> Ty<'db> { + match fields.kind(db) { + TyKind::Tuple(elems) => product_rep_ty(db, elems.clone()), + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Ty::unit(db), + _ => fields, + } +} + +fn product_rep_ty<'db>(db: &'db dyn Db, fields: Vec>) -> Ty<'db> { + let mut fields = fields.into_iter(); + let Some(first) = fields.next() else { + return Ty::unit(db); + }; + let rest = fields.collect::>(); + if rest.is_empty() { + first + } else { + Ty::named( + db, + TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + vec![first, product_rep_ty(db, rest)], + ) + } +} + +fn sum_rep_ty<'db>(db: &'db dyn Db, mut reps: Vec>) -> Ty<'db> { + match reps.len() { + 0 => Ty::unit(db), + 1 => reps.pop().expect("one rep"), + _ => { + let first = reps.remove(0); + Ty::named( + db, + TyCtor::Builtin(crate::BuiltinTyCtor::Sum), + vec![first, sum_rep_ty(db, reps)], + ) + } + } +} diff --git a/crates/hir-ty/src/solver/display.rs b/crates/hir-ty/src/solver/display.rs new file mode 100644 index 00000000..b2e8c3a4 --- /dev/null +++ b/crates/hir-ty/src/solver/display.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::display::{display_pred_source, display_ty_source, display_var_name}; + +pub(super) fn display_vars(vars: &[u32], names: &[String]) -> Vec { + vars.iter() + .map(|var| display_var_name(*var, names)) + .collect::>() +} + +pub(super) fn display_scheme_source<'db>( + db: &'db dyn Db, + scheme: TyScheme<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], +) -> String { + let names = type_vars + .iter() + .map(|var| (*var.name.atom()).text(db).to_owned()) + .collect::>(); + let body = scheme.body(db); + let preds = body + .preds(db) + .iter() + .map(|pred| display_pred_source(db, *pred, &names)) + .collect::>(); + let ty = display_ty_source(db, body.ty(db), &names); + let qualified = if preds.is_empty() { + ty + } else { + format!("{ty} where {}", preds.join(", ")) + }; + if scheme.binder_count(db) == 0 { + qualified + } else { + let vars = (0..scheme.binder_count(db)) + .map(|index| display_var_name(index, &names)) + .collect::>() + .join(", "); + format!("<{vars}> {qualified}") + } +} diff --git a/crates/hir-ty/src/solver/engine.rs b/crates/hir-ty/src/solver/engine.rs new file mode 100644 index 00000000..8418f7fe --- /dev/null +++ b/crates/hir-ty/src/solver/engine.rs @@ -0,0 +1,434 @@ +use std::convert::Infallible; + +use tablesolve::{ + AnswerlessMode, Canonical as TabledCanonical, CanonicalizeResult, ClausesResult, + Config as TabledConfig, ContextTransition, Limits, ReportOptions, ResolutionContext, + Scheduling, Transition, +}; + +use super::*; + +// Tabled resolution can otherwise spend the entire work-item fuel budget on a +// strictly type-growing chain (`C(a) => C(Box(a))`), retaining and repeatedly +// canonicalizing increasingly large goals. Real programs share subgoals and +// stay far below this bound; the separate cap keeps pathological growth cheap +// without charging rigid-head-prefiltered clauses against useful solver fuel. +const MAX_TABLE_ENTRIES: usize = 1_024; + +/// Solcore's language adapter for the shared tabled-resolution engine. +pub(super) struct TabledEngine<'db> { + db: &'db dyn Db, + env: TraitEnvId<'db>, + /// Variables fixed by the surrounding checked body; never solved by the + /// engine and tracked by stable origin across canonicalization. + local_context_vars: Vec, + /// Work-item budget retained for compatibility with `SolverReport`. + fuel: usize, +} + +impl<'db> TabledEngine<'db> { + pub(super) fn new(db: &'db dyn Db, env: TraitEnvId<'db>, fuel: usize) -> Self { + let mut local_context_vars = FxHashSet::default(); + for pred in env.local_givens(db) { + collect_pred_vars(db, *pred, &mut local_context_vars); + } + let mut local_context_vars = local_context_vars.into_iter().collect::>(); + local_context_vars.sort_unstable(); + let local_context_vars = local_context_vars + .into_iter() + .map(RigidVar::identity) + .collect(); + Self { + db, + env, + local_context_vars, + fuel, + } + } + + /// Resolve `goal` through `tablesolve`, mapping the generic engine report + /// back to the solver's existing result and counter types. + pub(super) fn run( + &mut self, + goal: Pred<'db>, + allowed_goal_vars: &FxHashSet, + ) -> EngineResult<'db> { + let root = SolverGoal { + pred: goal, + allowed_vars: allowed_goal_vars.clone(), + rigid_vars: self.local_context_vars.clone(), + }; + let config = TabledConfig { + scheduling: Scheduling::Fair, + limits: Limits { + max_steps: Some(self.fuel), + max_tables: Some(MAX_TABLE_ENTRIES), + max_root_answers: None, + max_pending_work: None, + }, + }; + let report_options = ReportOptions::new().with_answerless(AnswerlessMode::Omit); + let report = tablesolve::solve_with_options(self, root, config, report_options) + .unwrap_or_else(|error: Infallible| match error {}); + let stats = SolverStats { + table_size: report.stats.tables_created, + generator_steps: report.stats.clauses_tried, + answers_found: report.stats.answers_added, + }; + let exhausted = report.resource_exhausted(); + let fuel_remaining = self.fuel.saturating_sub(report.stats.steps); + EngineResult { + answers: report.answers, + exhausted, + fuel_remaining, + stats, + } + } + + /// Program clauses eligible for `key`, in resolution order: local givens, + /// then non-default instances, then superclass projections, and — only + /// when no non-default clause head can unify with the goal — defaults. + fn applicable_clauses(&self, key: &TableKey<'db>) -> Vec> { + // This is a one-way prefilter. Variables and their correlations are + // deliberately ignored, so impossible clauses may remain but an + // applicable clause is never discarded. + let head_can_apply = |clause: &ProgramClause<'db>| { + pred_head_shapes_may_match(self.db, clause.head, key.pred) + }; + let mut clauses = Vec::new(); + clauses.extend( + self.env + .local_givens(self.db) + .iter() + .copied() + .map(|given| ProgramClause { + binder_count: 0, + head: canonicalize_local_given(self.db, given, key), + conditions: Vec::new(), + origin: ClauseOrigin::Given, + }) + .filter(&head_can_apply), + ); + let base_clauses = self.env.clauses_for_pred(self.db, key.pred); + clauses.extend(base_clauses.iter().filter_map(|clause| { + (!clause.origin.is_default() + && !matches!(clause.origin, ClauseOrigin::Superclass(_)) + && head_can_apply(clause)) + .then_some(clause.clone()) + })); + clauses.extend(base_clauses.iter().filter_map(|clause| { + (!clause.origin.is_default() + && matches!(clause.origin, ClauseOrigin::Superclass(_)) + && head_can_apply(clause)) + .then_some(clause.clone()) + })); + + // Default selection is local to each tabled subgoal. A non-default + // instance may itself rely on a condition discharged by a default. + let default_clauses = base_clauses + .iter() + .filter(|clause| clause.origin.is_default() && head_can_apply(clause)) + .cloned() + .collect::>(); + if !default_clauses.is_empty() && !self.has_non_default_unifying_head(key, base_clauses) { + clauses.extend(default_clauses); + } + clauses + } + + fn has_non_default_unifying_head( + &self, + key: &TableKey<'db>, + base_clauses: &[ProgramClause<'db>], + ) -> bool { + let mut goal_vars = key.allowed_vars(); + collect_pred_vars(self.db, key.pred, &mut goal_vars); + base_clauses.iter().any(|clause| { + !clause.origin.is_default() + && !matches!(clause.origin, ClauseOrigin::Superclass(_)) + && head_can_unify(self.db, clause, key.pred, &goal_vars) + }) + } + + fn suspend(&self, state: ConsumerState<'db>) -> ContextTransition { + let condition = state + .subst + .apply_pred(self.db, state.clause.conditions[state.next_condition]); + let goal = SolverGoal { + pred: condition, + allowed_vars: state.condition_vars.clone(), + rigid_vars: state.rigid_vars.clone(), + }; + Transition::Suspend { goal, state } + } + + fn answer( + &self, + key: &TableKey<'db>, + clause: &InstantiatedClause<'db>, + subst: MatchSubst<'db>, + sub_evidence: Vec>, + ) -> Answer<'db> { + let evidence = clause_evidence(self.db, key.pred, clause, &subst, sub_evidence); + Answer { + candidate: Candidate { + subst: subst.snapshot_for_vars(self.db, key.flex_count), + evidence: apply_evidence(self.db, evidence, &subst), + }, + origin: clause.origin.clone(), + } + } +} + +impl<'db> ResolutionContext for TabledEngine<'db> { + type Goal = SolverGoal<'db>; + type Key = TableKey<'db>; + type Clause = ProgramClause<'db>; + type Answer = Answer<'db>; + type AnswerKey = (Substitution<'db>, ClauseOrigin<'db>); + type Output = Answer<'db>; + type State = ConsumerState<'db>; + type Rebase = GoalRenaming; + type Error = Infallible; + type StopReason = Infallible; + + fn canonicalize( + &mut self, + goal: Self::Goal, + ) -> CanonicalizeResult { + let (key, rebase) = + canonicalize_goal(self.db, goal.pred, &goal.allowed_vars, &goal.rigid_vars); + Ok(TabledCanonical::new(key, rebase).into()) + } + + fn clauses( + &mut self, + key: &Self::Key, + ) -> ClausesResult { + Ok(self.applicable_clauses(key).into()) + } + + fn apply_clause( + &mut self, + key: &Self::Key, + clause: Self::Clause, + ) -> Result, Self::Error> { + let allowed_goal_vars = key.allowed_vars(); + let avoid_vars = key.canonical_context_vars(); + let instantiated = instantiate_clause(self.db, &clause, key.pred, &avoid_vars); + let Some(subst) = match_head( + self.db, + instantiated.head, + key.pred, + &instantiated.binder_vars, + &allowed_goal_vars, + ) else { + return Ok(Transition::Reject); + }; + + let mut condition_vars = allowed_goal_vars; + condition_vars.extend(instantiated.binder_vars.iter().copied()); + if instantiated.conditions.is_empty() { + return Ok(Transition::Answer(self.answer( + key, + &instantiated, + subst, + Vec::new(), + ))); + } + + Ok(self.suspend(ConsumerState { + clause: instantiated, + subst, + sub_evidence: Vec::new(), + next_condition: 0, + condition_vars, + rigid_vars: key.rigid_vars().to_vec(), + })) + } + + fn resume( + &mut self, + parent: &Self::Key, + mut state: Self::State, + answer: Self::Answer, + rebase: Self::Rebase, + ) -> Result, Self::Error> { + let alternative = actualize_answer(self.db, &answer, &rebase); + let mut combined_subst = state.subst.clone(); + if !combined_subst.merge(self.db, &alternative.candidate.subst) { + return Ok(Transition::Reject); + } + for (_, ty) in &alternative.candidate.subst.values { + collect_ty_vars(self.db, *ty, &mut state.condition_vars); + } + state.sub_evidence.push(apply_evidence( + self.db, + alternative.candidate.evidence, + &combined_subst, + )); + state.subst = combined_subst; + state.next_condition += 1; + if state.next_condition < state.clause.conditions.len() { + Ok(self.suspend(state)) + } else { + Ok(Transition::Answer(self.answer( + parent, + &state.clause, + state.subst, + state.sub_evidence, + ))) + } + } + + fn rebase_answer( + &mut self, + answer: &Self::Answer, + rebase: &Self::Rebase, + ) -> Result { + Ok(actualize_answer(self.db, answer, rebase)) + } + + fn answer_key(&self, _key: &Self::Key, answer: &Self::Answer) -> Self::AnswerKey { + (answer.candidate.subst.clone(), answer.origin.clone()) + } +} + +#[derive(Clone)] +pub(super) struct SolverGoal<'db> { + pred: Pred<'db>, + allowed_vars: FxHashSet, + rigid_vars: Vec, +} + +/// A partially solved clause retained by `tablesolve` while it waits for the +/// current condition's table to produce answers. +#[derive(Clone)] +pub(super) struct ConsumerState<'db> { + clause: InstantiatedClause<'db>, + subst: MatchSubst<'db>, + sub_evidence: Vec>, + next_condition: usize, + condition_vars: FxHashSet, + /// Stable rigid origins mapped into the parent subgoal's coordinates. + rigid_vars: Vec, +} + +pub(super) struct EngineResult<'db> { + pub(super) answers: Vec>, + pub(super) exhausted: bool, + pub(super) fuel_remaining: usize, + pub(super) stats: SolverStats, +} + +/// One answer for a subgoal: a substitution over its flex variables plus the +/// evidence that discharges the goal, tagged with the clause it came from. +#[derive(Clone, PartialEq, Eq, Hash)] +pub(super) struct Answer<'db> { + pub(super) candidate: Candidate<'db>, + pub(super) origin: ClauseOrigin<'db>, +} + +fn pred_head_shapes_may_match<'db>(db: &'db dyn Db, lhs: Pred<'db>, rhs: Pred<'db>) -> bool { + match (lhs.kind(db), rhs.kind(db)) { + ( + PredKind::InClass { + class: lhs_class, + main: lhs_main, + args: lhs_args, + }, + PredKind::InClass { + class: rhs_class, + main: rhs_main, + args: rhs_args, + }, + ) if lhs_class == rhs_class && lhs_args.len() == rhs_args.len() => { + ty_shapes_may_match(db, *lhs_main, *rhs_main) + && lhs_args + .iter() + .zip(rhs_args) + .all(|(lhs_arg, rhs_arg)| ty_shapes_may_match(db, *lhs_arg, *rhs_arg)) + } + ( + PredKind::Eq { + lhs: lhs_l, + rhs: lhs_r, + }, + PredKind::Eq { + lhs: rhs_l, + rhs: rhs_r, + }, + ) => ty_shapes_may_match(db, *lhs_l, *rhs_l) && ty_shapes_may_match(db, *lhs_r, *rhs_r), + (PredKind::Error, PredKind::Error) => true, + _ => false, + } +} + +fn ty_shapes_may_match<'db>(db: &'db dyn Db, lhs: Ty<'db>, rhs: Ty<'db>) -> bool { + if let TyKind::Comptime(inner) = lhs.kind(db) { + return ty_shapes_may_match(db, *inner, rhs); + } + if let TyKind::Comptime(inner) = rhs.kind(db) { + return ty_shapes_may_match(db, lhs, *inner); + } + + match (lhs.kind(db), rhs.kind(db)) { + // Correlations between variables are intentionally ignored. This is a + // one-way prefilter: false positives cost fuel, while false negatives + // would make resolution incomplete. + (TyKind::BoundVar(_), _) | (_, TyKind::BoundVar(_)) => true, + (TyKind::Error | TyKind::Unknown, _) | (_, TyKind::Error | TyKind::Unknown) => true, + ( + TyKind::Named { + ctor: lhs_ctor, + args: lhs_args, + }, + TyKind::Named { + ctor: rhs_ctor, + args: rhs_args, + }, + ) if lhs_ctor == rhs_ctor && lhs_args.len() == rhs_args.len() => lhs_args + .iter() + .zip(rhs_args) + .all(|(lhs_arg, rhs_arg)| ty_shapes_may_match(db, *lhs_arg, *rhs_arg)), + ( + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + TyKind::Tuple(elems), + ) + | ( + TyKind::Tuple(elems), + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + ) if args.is_empty() && elems.is_empty() => true, + ( + TyKind::Function { + params: lhs_params, + ret: lhs_ret, + }, + TyKind::Function { + params: rhs_params, + ret: rhs_ret, + }, + ) if lhs_params.len() == rhs_params.len() => { + lhs_params + .iter() + .zip(rhs_params) + .all(|(lhs_param, rhs_param)| ty_shapes_may_match(db, *lhs_param, *rhs_param)) + && ty_shapes_may_match(db, *lhs_ret, *rhs_ret) + } + (TyKind::Tuple(lhs_elems), TyKind::Tuple(rhs_elems)) + if lhs_elems.len() == rhs_elems.len() => + { + lhs_elems + .iter() + .zip(rhs_elems) + .all(|(lhs_elem, rhs_elem)| ty_shapes_may_match(db, *lhs_elem, *rhs_elem)) + } + _ => false, + } +} diff --git a/crates/hir-ty/src/solver/env.rs b/crates/hir-ty/src/solver/env.rs new file mode 100644 index 00000000..69cdc98b --- /dev/null +++ b/crates/hir-ty/src/solver/env.rs @@ -0,0 +1,671 @@ +use super::derived_generic::AdtDeriveInfo; +use super::*; + +#[salsa::tracked] +pub fn trait_env_for_module<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> TraitEnvId<'db> { + if let Some(file) = db.module_file(module) { + let source = parse_file_to_hir(db, file).module(db); + let hir_module = crate::prepare_module(db, source).module(db); + if hir_module != source { + let env = nameres::module_env_for_hir_module(db, module, hir_module); + if let Some(item_scope) = env.item_scope.clone() { + let resolution = + hir_nameres::resolve_module_with_imports(db, hir_module, item_scope, &env); + return trait_env_from_module_resolution_and_imports( + db, + hir_module, + &resolution, + &env.import_surface(), + ); + } + } + } + + let env = nameres::module_import_surface(db, module); + + let mut modules = Vec::new(); + modules.push(module); + modules.extend(env.instances.iter().map(|origin| origin.module)); + modules.extend(visible_class_modules(db, &env)); + + let source = ModuleTraitEnvSource { + superclass_modules: unique_modules(modules), + instance_origins: env.instances.clone(), + derived_generic: visible_generic_class(db, &env) + .map(|generic| DerivedGenericClauseSource { module, generic }), + }; + TraitEnvId::new( + db, + BaseTraitEnvId::new(db, BaseTraitEnvSource::Module(source)), + LocalGivensId::new(db, Vec::new()), + ) +} + +/// Builds a trait environment from an already resolved HIR module. +/// +/// This is primarily useful for tests and direct HIR clients that do not have a +/// logical [`ModuleId`] available. +pub fn trait_env_from_module_resolution<'db>( + db: &'db dyn Db, + module: Module<'db>, + module_resolution: &hir_nameres::ModuleResolutionMap<'db>, +) -> TraitEnvId<'db> { + let mut clause_sets = Vec::new(); + clause_sets.push(builtin_trait_clause_set(db)); + + let mut superclass_builder = TraitClauseBuilder::new(db); + superclass_builder.add_module_superclasses(module, &module_resolution.item_resolutions); + clause_sets.push(superclass_builder.finish()); + + for item in module.items(db) { + if let Item::InstanceDef(instance) = item { + let mut instance_builder = TraitClauseBuilder::new(db); + instance_builder.add_instance(module, *instance, &module_resolution.item_resolutions); + clause_sets.push(instance_builder.finish()); + } + } + if let Some(generic) = local_generic_class(db, module) + .or_else(|| imported_generic_class(db, &module_resolution.item_resolutions)) + { + let mut derived_builder = TraitClauseBuilder::new(db); + derived_builder.add_derived_generic_instances( + module, + &module_resolution.item_resolutions, + generic, + ); + clause_sets.push(derived_builder.finish()); + } + TraitEnvId::new( + db, + BaseTraitEnvId::new(db, BaseTraitEnvSource::Resolved { clause_sets }), + LocalGivensId::new(db, Vec::new()), + ) +} + +/// Builds a trait environment for an already resolved HIR module with an +/// explicit imported-name surface. +pub fn trait_env_from_module_resolution_and_imports<'db>( + db: &'db dyn Db, + module: Module<'db>, + module_resolution: &hir_nameres::ModuleResolutionMap<'db>, + imports: &nameres::ModuleImportSurface<'db>, +) -> TraitEnvId<'db> { + let module_id = imports + .owner + .and_then(|_| nameres::module_id_for_source_file(db, module.def_id_value(db).file(db))); + trait_env_from_module_resolution_and_imports_impl( + db, + module_id, + module, + module_resolution, + imports, + ) +} + +fn trait_env_from_module_resolution_and_imports_impl<'db>( + db: &'db dyn Db, + module_id: Option>, + module: Module<'db>, + module_resolution: &hir_nameres::ModuleResolutionMap<'db>, + imports: &nameres::ModuleImportSurface<'db>, +) -> TraitEnvId<'db> { + let mut clause_sets = Vec::new(); + clause_sets.push(builtin_trait_clause_set(db)); + + let mut superclass_builder = TraitClauseBuilder::new(db); + superclass_builder.add_module_superclasses(module, &module_resolution.item_resolutions); + clause_sets.push(superclass_builder.finish()); + for module in unique_modules(visible_class_modules(db, imports)) { + clause_sets.push(module_superclass_clause_set(db, module)); + } + + let shared_local_facts = module_id + .and_then(|module| module_instance_facts(db, module).as_ref()) + .filter(|facts| { + facts.module == module && facts.item_resolutions == *module_resolution.item_resolutions + }); + for item in module.items(db) { + if let Item::InstanceDef(instance) = item { + let mut instance_builder = TraitClauseBuilder::new(db); + if let Some(fact) = shared_local_facts.and_then(|facts| { + facts + .instances + .iter() + .find(|fact| fact.def == instance.def_id_value(db)) + }) { + instance_builder.add_instance_fact(fact); + } else { + instance_builder.add_instance( + module, + *instance, + &module_resolution.item_resolutions, + ); + } + clause_sets.push(instance_builder.finish()); + } + } + for origin in &imports.instances { + let mut instance_builder = TraitClauseBuilder::new(db); + if let Some(fact) = instance_fact_for_origin(db, origin.module, origin.def_id) { + instance_builder.add_instance_fact(fact); + } + clause_sets.push(instance_builder.finish()); + } + + if let Some(generic) = local_generic_class(db, module) + .or_else(|| imported_generic_class(db, &module_resolution.item_resolutions)) + .or_else(|| visible_generic_class(db, imports)) + { + let mut derived_builder = TraitClauseBuilder::new(db); + derived_builder.add_derived_generic_instances( + module, + &module_resolution.item_resolutions, + generic, + ); + clause_sets.push(derived_builder.finish()); + } + TraitEnvId::new( + db, + BaseTraitEnvId::new(db, BaseTraitEnvSource::Resolved { clause_sets }), + LocalGivensId::new(db, Vec::new()), + ) +} + +/// Extends an existing trait environment with local given predicates. +pub fn trait_env_with_givens<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + givens: Vec>, +) -> TraitEnvId<'db> { + let mut local_givens = env.local_givens(db).clone(); + local_givens.extend(givens); + TraitEnvId::new( + db, + env.base(db), + LocalGivensId::new(db, unique_preds(local_givens)), + ) +} + +#[salsa::tracked(returns(ref))] +pub(super) fn base_trait_env_clauses<'db>( + db: &'db dyn Db, + base: BaseTraitEnvId<'db>, +) -> Vec> { + match base.source(db) { + BaseTraitEnvSource::Module(source) => { + let mut clauses = Vec::new(); + extend_clause_set(&mut clauses, db, builtin_trait_clause_set(db)); + for module in &source.superclass_modules { + extend_clause_set(&mut clauses, db, module_superclass_clause_set(db, *module)); + } + for origin in &source.instance_origins { + if let Some(fact) = instance_fact_for_origin(db, origin.module, origin.def_id) { + clauses.push(fact.clause()); + } + } + if let Some(source) = source.derived_generic { + extend_clause_set( + &mut clauses, + db, + derived_generic_clause_set(db, source.module, source.generic), + ); + } + clauses + } + BaseTraitEnvSource::Resolved { clause_sets } => { + let mut clauses = Vec::new(); + for set in clause_sets { + extend_clause_set(&mut clauses, db, *set); + } + clauses + } + } +} + +/// Returns only the base clauses whose head belongs to `class`. +/// +/// Most solver goals are class predicates. Memoizing this projection keeps the +/// enlarged imported environment from being scanned for every subgoal and +/// shares the result across solves that layer different local givens over the +/// same base environment. +#[salsa::tracked(returns(ref))] +pub(super) fn base_trait_env_class_clauses<'db>( + db: &'db dyn Db, + base: BaseTraitEnvId<'db>, + class: ClassId<'db>, +) -> Vec> { + base_trait_env_clauses(db, base) + .iter() + .filter(|clause| { + matches!( + clause.head.kind(db), + PredKind::InClass { + class: clause_class, + .. + } if *clause_class == class + ) + }) + .cloned() + .collect() +} + +fn extend_clause_set<'db>( + clauses: &mut Vec>, + db: &'db dyn Db, + set: TraitClauseSetId<'db>, +) { + clauses.extend(set.clauses(db).iter().cloned()); +} + +#[salsa::tracked] +fn builtin_trait_clause_set<'db>(db: &'db dyn Db) -> TraitClauseSetId<'db> { + let mut builder = TraitClauseBuilder::new(db); + builder.add_builtin_instances(); + builder.finish() +} + +#[salsa::tracked] +fn module_superclass_clause_set<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> TraitClauseSetId<'db> { + let mut builder = TraitClauseBuilder::new(db); + if let Some((scope, item_resolutions)) = scope_resolution_for_module_id(db, module) { + builder.add_module_superclasses(scope.module, &item_resolutions); + } + builder.finish() +} + +fn instance_fact_for_origin<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def_id: DefId<'db>, +) -> Option<&'db InstanceFact<'db>> { + module_instance_facts(db, module) + .as_ref()? + .instances + .iter() + .find(|fact| fact.def == def_id) +} + +#[salsa::tracked] +fn derived_generic_clause_set<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + generic: DefId<'db>, +) -> TraitClauseSetId<'db> { + let mut builder = TraitClauseBuilder::new(db); + if let Some((scope, item_resolutions)) = scope_resolution_for_module_id(db, module) { + builder.add_derived_generic_instances(scope.module, &item_resolutions, generic); + } + builder.finish() +} + +struct TraitClauseBuilder<'db> { + db: &'db dyn Db, + clauses: Vec>, +} + +impl<'db> TraitClauseBuilder<'db> { + fn new(db: &'db dyn Db) -> Self { + Self { + db, + clauses: Vec::new(), + } + } + + fn finish(self) -> TraitClauseSetId<'db> { + TraitClauseSetId::new(self.db, self.clauses) + } + + fn add_builtin_instances(&mut self) { + let int = ClassId::Builtin(BuiltinClassId::Int); + for ty in [Ty::word(self.db), Ty::integer(self.db)] { + self.clauses.push(ProgramClause { + binder_count: 0, + head: Pred::in_class(self.db, int, ty, Vec::new()), + conditions: Vec::new(), + origin: ClauseOrigin::Builtin, + }); + } + self.add_builtin_function_invokables(); + } + + fn add_builtin_function_invokables(&mut self) { + let invokable = ClassId::Builtin(BuiltinClassId::Invokable); + for arity in 0..=8 { + let params = (0..arity) + .map(|index| Ty::bound(self.db, index)) + .collect::>(); + let ret = Ty::bound(self.db, arity); + let main = Ty::function(self.db, params.clone(), ret); + self.clauses.push(ProgramClause { + binder_count: arity + 1, + head: Pred::in_class( + self.db, + invokable, + main, + vec![invokable_arg_ty(self.db, params.clone()), ret], + ), + conditions: Vec::new(), + origin: ClauseOrigin::Builtin, + }); + if arity > 1 { + self.clauses.push(ProgramClause { + binder_count: arity + 1, + head: Pred::in_class( + self.db, + invokable, + main, + vec![Ty::tuple(self.db, params.clone()), ret], + ), + conditions: Vec::new(), + origin: ClauseOrigin::Builtin, + }); + if arity > 2 { + self.clauses.push(ProgramClause { + binder_count: arity + 1, + head: Pred::in_class( + self.db, + invokable, + main, + vec![nested_tuple_arg_ty(self.db, params), ret], + ), + conditions: Vec::new(), + origin: ClauseOrigin::Builtin, + }); + } + } + } + } + + fn add_module_superclasses( + &mut self, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + ) { + for item in module.items(self.db) { + if let Item::ClassDef(class) = item { + self.add_class_superclasses(module, *class, item_resolutions); + } + } + } + + fn add_class_superclasses( + &mut self, + module: Module<'db>, + class: ClassDef<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + ) { + let type_vars = + type_var_bindings(class.def_id_value(self.db), class.type_var_elems(self.db)); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut normalizer = AliasNormalizer::new(self.db, module, item_resolutions); + let class_head = normalizer.normalize_pred(lowerer.lower_pred(class.head(self.db))); + for super_pred in class.super_preds(self.db) { + self.clauses.push(ProgramClause { + binder_count: type_vars.len() as u32, + head: normalizer.normalize_pred(lowerer.lower_pred(*super_pred)), + conditions: vec![class_head], + origin: ClauseOrigin::Superclass(class.def_id_value(self.db)), + }); + } + } + + fn add_instance( + &mut self, + module: Module<'db>, + instance: InstanceDef<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + ) { + let type_vars = type_var_bindings( + instance.def_id_value(self.db), + instance.type_var_elems(self.db), + ); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut normalizer = AliasNormalizer::new(self.db, module, item_resolutions); + let head = normalizer.normalize_pred(lowerer.lower_pred(instance.head(self.db))); + let conditions = instance + .preds(self.db) + .iter() + .map(|pred| normalizer.normalize_pred(lowerer.lower_pred(*pred))) + .collect(); + + // Instance soundness checks are intentionally run by the module-level + // `instance_soundness_diagnostics` query, not while building clauses. + self.clauses.push(ProgramClause { + binder_count: type_vars.len() as u32, + head, + conditions, + origin: ClauseOrigin::Instance { + def: instance.def_id_value(self.db), + default: instance.default_kw(self.db).is_some(), + }, + }); + } + + fn add_instance_fact(&mut self, fact: &InstanceFact<'db>) { + self.clauses.push(fact.clause()); + } + + fn add_derived_generic_instances( + &mut self, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + generic: DefId<'db>, + ) { + let mut seen = FxHashSet::default(); + for info in local_adt_infos(self.db, module) { + seen.insert(info.adt.def_id_value(self.db)); + let Some(plan) = derived_generic_instance_plan_with_resolutions( + self.db, + module, + item_resolutions, + &info, + generic, + ) else { + continue; + }; + self.push_derived_generic_clause(&info, &plan, generic); + } + + // Imported ADTs referenced by signatures need definition-side + // derived evidence during frontend type checking. Reconstructing it in + // the specializer is too late for generated std.dispatch obligations. + let mut pending = VecDeque::new(); + for resolution in &item_resolutions.types { + match &resolution.resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Adt, + } => pending.push_back(*def), + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::TypeAlias, + } => { + let alias_module = + parse_file_to_hir(self.db, def.file(self.db)).module(self.db); + let Some((inherited_count, explicit_count)) = + type_alias_binder_counts(self.db, alias_module, *def) + else { + continue; + }; + let alias = Ty::named( + self.db, + TyCtor::User(crate::UserTyCtor { + def: *def, + kind: crate::UserTyCtorKind::Alias, + }), + (0..explicit_count) + .map(|index| Ty::bound(self.db, inherited_count + index)) + .collect(), + ); + let normalized = + AliasNormalizer::new(self.db, module, item_resolutions).normalize_ty(alias); + collect_adt_defs_from_ty(self.db, normalized, &mut pending); + } + _ => {} + } + } + + // A directly referenced imported ADT can expose more imported ADTs in + // its derived representation. Close that dependency graph here so the + // generated ABI obligations see every definition-side `Generic` + // clause. Walking type arguments is significant for representations + // such as `Box(Inner)`, where `Inner` is not the representation head. + while let Some(def) = pending.pop_front() { + if !seen.insert(def) { + continue; + } + let definition_module = parse_file_to_hir(self.db, def.file(self.db)).module(self.db); + let Some(info) = local_adt_infos(self.db, definition_module) + .into_iter() + .find(|info| info.adt.def_id_value(self.db) == def) + else { + continue; + }; + let Some(plan) = + derived_generic_instance_plan(self.db, definition_module, info.adt, generic) + else { + continue; + }; + collect_adt_defs_from_ty(self.db, plan.rep, &mut pending); + self.push_derived_generic_clause(&info, &plan, generic); + } + } + + fn push_derived_generic_clause( + &mut self, + info: &AdtDeriveInfo<'db>, + plan: &DerivedGenericPlan<'db>, + generic: DefId<'db>, + ) { + let params = info + .adt + .ty_param_elems(self.db) + .iter() + .enumerate() + .map(|(index, _)| Ty::bound(self.db, index as u32)) + .collect::>(); + let main = Ty::named( + self.db, + TyCtor::User(crate::UserTyCtor { + def: info.adt.def_id_value(self.db), + kind: crate::UserTyCtorKind::Adt, + }), + params, + ); + self.clauses.push(ProgramClause { + binder_count: info.type_vars.len() as u32, + head: Pred::in_class(self.db, ClassId::User(generic), main, vec![plan.rep]), + conditions: Vec::new(), + origin: ClauseOrigin::Derived(DerivedClauseKind::Generic { + adt: info.adt.def_id_value(self.db), + }), + }); + } +} + +fn type_alias_binder_counts<'db>( + db: &'db dyn Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option<(u32, u32)> { + module + .items(db) + .iter() + .find_map(|item| type_alias_binder_counts_in_item(db, *item, def, 0)) +} + +fn type_alias_binder_counts_in_item<'db>( + db: &'db dyn Db, + item: Item<'db>, + def: DefId<'db>, + inherited: u32, +) -> Option<(u32, u32)> { + match item { + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + Some((inherited, alias.ty_param_elems(db).len() as u32)) + } + Item::ContractDef(contract) => { + let inherited = inherited + contract.ty_param_elems(db).len() as u32; + contract.items(db).iter().find_map(|item| match *item { + ContractItem::TypeAlias(alias) => { + type_alias_binder_counts_in_item(db, Item::TypeAlias(alias), def, inherited) + } + ContractItem::FunctionDef(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }) + } + _ => None, + } +} + +fn collect_adt_defs_from_ty<'db>(db: &'db dyn Db, ty: Ty<'db>, defs: &mut VecDeque>) { + match ty.kind(db) { + TyKind::Named { ctor, args } => { + if let TyCtor::User(crate::UserTyCtor { + def, + kind: crate::UserTyCtorKind::Adt, + }) = ctor + { + defs.push_back(*def); + } + for arg in args { + collect_adt_defs_from_ty(db, *arg, defs); + } + } + TyKind::Function { params, ret } => { + for param in params { + collect_adt_defs_from_ty(db, *param, defs); + } + collect_adt_defs_from_ty(db, *ret, defs); + } + TyKind::Tuple(elems) => { + for elem in elems { + collect_adt_defs_from_ty(db, *elem, defs); + } + } + TyKind::Comptime(inner) => collect_adt_defs_from_ty(db, *inner, defs), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => {} + } +} + +fn invokable_arg_ty<'db>(db: &'db dyn Db, params: Vec>) -> Ty<'db> { + let mut params = params.into_iter(); + let Some(first) = params.next() else { + return Ty::unit(db); + }; + let rest = params.collect::>(); + if rest.is_empty() { + first + } else { + Ty::named( + db, + TyCtor::Builtin(crate::BuiltinTyCtor::Pair), + vec![first, invokable_arg_ty(db, rest)], + ) + } +} + +fn nested_tuple_arg_ty<'db>(db: &'db dyn Db, params: Vec>) -> Ty<'db> { + let mut params = params.into_iter(); + let Some(first) = params.next() else { + return Ty::unit(db); + }; + let rest = params.collect::>(); + if rest.is_empty() { + first + } else { + Ty::tuple(db, vec![first, nested_tuple_arg_ty(db, rest)]) + } +} diff --git a/crates/hir-ty/src/solver/evidence.rs b/crates/hir-ty/src/solver/evidence.rs new file mode 100644 index 00000000..af802e92 --- /dev/null +++ b/crates/hir-ty/src/solver/evidence.rs @@ -0,0 +1,252 @@ +use super::*; + +impl<'db> Evidence<'db> { + /// Returns a short evidence snapshot for diagnostics and tests. + pub fn display(&self, db: &'db dyn HirDb) -> String { + match self { + Evidence::Instance { + instance, + args, + sub_evidence, + } => { + let name = instance + .name(db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("{:?}", instance.kind(db))); + let args = args + .iter() + .map(|arg| arg.display(db)) + .collect::>() + .join(", "); + let head = if args.is_empty() { + name + } else { + format!("{name}<{args}>") + }; + if sub_evidence.is_empty() { + format!("impl {head}") + } else { + format!("impl {head} with {} subproof(s)", sub_evidence.len()) + } + } + Evidence::Builtin { pred } => format!("builtin {}", pred.display(db)), + Evidence::Superclass { class, pred, child } => { + let name = class + .name(db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("{:?}", class.kind(db))); + format!( + "supertrait {name}: {} via {}", + pred.display(db), + child.display(db) + ) + } + Evidence::Derived { + kind, + pred, + sub_evidence, + } => { + if sub_evidence.is_empty() { + format!("derived {kind:?} {}", pred.display(db)) + } else { + format!( + "derived {kind:?} {} with {} subproof(s)", + pred.display(db), + sub_evidence.len() + ) + } + } + } + } +} + +pub(super) fn solution_from_answers<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + answers: Vec>, +) -> Solution<'db> { + let mut seen_answers = FxHashSet::default(); + let answers = answers + .into_iter() + .filter(|answer| seen_answers.insert(answer.clone())) + .collect::>(); + let Some(best_priority) = answers + .iter() + .map(|answer| answer_priority(db, env, answer)) + .min() + else { + return Solution::NoSolution; + }; + + let mut seen_candidates = FxHashSet::default(); + let mut candidates = Vec::new(); + for answer in answers { + if answer_priority(db, env, &answer) != best_priority { + continue; + } + if seen_candidates.insert(( + answer_root(db, env, &answer), + answer.candidate.subst.clone(), + )) { + candidates.push(answer.candidate); + } + } + + match candidates.as_slice() { + [] => Solution::NoSolution, + [candidate] => Solution::Unique { + subst: candidate.subst.clone(), + evidence: candidate.evidence.clone(), + }, + _ => Solution::Ambiguous { candidates }, + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum AnswerRoot<'db> { + Local(Pred<'db>), + Builtin(Pred<'db>), + Instance(DefId<'db>), + DefaultInstance(DefId<'db>), + Derived(DerivedClauseKind<'db>), + Superclass(DefId<'db>), + Other, +} + +fn answer_priority<'db>(db: &'db dyn Db, env: TraitEnvId<'db>, answer: &Answer<'db>) -> u8 { + if evidence_root_is_local_given(db, env, &answer.candidate.evidence) { + return 0; + } + match &answer.origin { + ClauseOrigin::Instance { default: true, .. } => 3, + ClauseOrigin::Superclass(_) => 2, + ClauseOrigin::Instance { default: false, .. } + | ClauseOrigin::Builtin + | ClauseOrigin::Derived(_) + | ClauseOrigin::Given => 1, + } +} + +fn answer_root<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + answer: &Answer<'db>, +) -> AnswerRoot<'db> { + if evidence_root_is_local_given(db, env, &answer.candidate.evidence) { + return evidence_root_pred(&answer.candidate.evidence) + .map(AnswerRoot::Local) + .unwrap_or(AnswerRoot::Other); + } + match &answer.origin { + ClauseOrigin::Instance { + def: instance, + default: true, + } => AnswerRoot::DefaultInstance(*instance), + ClauseOrigin::Instance { def: instance, .. } => AnswerRoot::Instance(*instance), + ClauseOrigin::Builtin => evidence_root_pred(&answer.candidate.evidence) + .map(AnswerRoot::Builtin) + .unwrap_or(AnswerRoot::Other), + ClauseOrigin::Derived(kind) => AnswerRoot::Derived(*kind), + ClauseOrigin::Given => evidence_root_pred(&answer.candidate.evidence) + .map(AnswerRoot::Local) + .unwrap_or(AnswerRoot::Other), + ClauseOrigin::Superclass(class) => AnswerRoot::Superclass(*class), + } +} + +fn evidence_root_is_local_given<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + evidence: &Evidence<'db>, +) -> bool { + match evidence { + Evidence::Builtin { pred } => env.local_givens(db).contains(pred), + Evidence::Superclass { child, .. } => evidence_root_is_local_given(db, env, child), + Evidence::Instance { .. } | Evidence::Derived { .. } => false, + } +} + +fn evidence_root_pred<'db>(evidence: &Evidence<'db>) -> Option> { + match evidence { + Evidence::Builtin { pred } + | Evidence::Superclass { pred, .. } + | Evidence::Derived { pred, .. } => Some(*pred), + Evidence::Instance { .. } => None, + } +} + +pub(super) fn clause_evidence<'db>( + db: &'db dyn Db, + goal: Pred<'db>, + clause: &InstantiatedClause<'db>, + subst: &MatchSubst<'db>, + sub_evidence: Vec>, +) -> Evidence<'db> { + match clause.origin { + ClauseOrigin::Instance { def: instance, .. } => Evidence::Instance { + instance, + args: subst.args_for_vars(db, &clause.binder_vars), + sub_evidence, + }, + ClauseOrigin::Builtin | ClauseOrigin::Given => Evidence::Builtin { pred: goal }, + ClauseOrigin::Derived(kind) => Evidence::Derived { + kind, + pred: goal, + sub_evidence, + }, + ClauseOrigin::Superclass(class) => Evidence::Superclass { + class, + pred: goal, + child: Box::new( + sub_evidence + .into_iter() + .next() + .unwrap_or(Evidence::Builtin { pred: goal }), + ), + }, + } +} + +pub(super) fn apply_evidence<'db>( + db: &'db dyn Db, + evidence: Evidence<'db>, + subst: &MatchSubst<'db>, +) -> Evidence<'db> { + match evidence { + Evidence::Instance { + instance, + args, + sub_evidence, + } => Evidence::Instance { + instance, + args: args + .into_iter() + .map(|arg| subst.apply_ty(db, arg)) + .collect(), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| apply_evidence(db, evidence, subst)) + .collect(), + }, + Evidence::Builtin { pred } => Evidence::Builtin { + pred: subst.apply_pred(db, pred), + }, + Evidence::Superclass { class, pred, child } => Evidence::Superclass { + class, + pred: subst.apply_pred(db, pred), + child: Box::new(apply_evidence(db, *child, subst)), + }, + Evidence::Derived { + kind, + pred, + sub_evidence, + } => Evidence::Derived { + kind, + pred: subst.apply_pred(db, pred), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| apply_evidence(db, evidence, subst)) + .collect(), + }, + } +} diff --git a/crates/hir-ty/src/solver/instance_facts.rs b/crates/hir-ty/src/solver/instance_facts.rs new file mode 100644 index 00000000..93de0288 --- /dev/null +++ b/crates/hir-ty/src/solver/instance_facts.rs @@ -0,0 +1,144 @@ +use super::*; + +/// One normalized condition attached to an instance declaration. +/// +/// Keeping normalization errors beside the predicate lets the soundness pass +/// preserve its source-order diagnostics without lowering the predicate a +/// second time after the solver clause has already been built. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub(super) struct InstanceConditionFact<'db> { + pub(super) pred: Pred<'db>, + pub(super) span: LabelSpan, + pub(super) alias_errors: Vec, +} + +/// Lowered, alias-normalized facts for one source instance. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub(super) struct InstanceFact<'db> { + pub(super) instance: InstanceDef<'db>, + pub(super) def: DefId<'db>, + pub(super) binder_count: u32, + pub(super) head: Pred<'db>, + pub(super) head_span: LabelSpan, + pub(super) head_alias_errors: Vec, + pub(super) conditions: Vec>, + pub(super) default: bool, +} + +impl<'db> InstanceFact<'db> { + pub(super) fn clause(&self) -> ProgramClause<'db> { + ProgramClause { + binder_count: self.binder_count, + head: self.head, + conditions: self + .conditions + .iter() + .map(|condition| condition.pred) + .collect(), + origin: ClauseOrigin::Instance { + def: self.def, + default: self.default, + }, + } + } + + pub(super) fn class(&self, db: &'db dyn Db) -> Option> { + match self.head.kind(db) { + PredKind::InClass { class, .. } => Some(*class), + PredKind::Eq { .. } | PredKind::Error => None, + } + } +} + +/// Module-wide instance facts shared by trait-environment construction and +/// soundness checking. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub(super) struct InstanceModuleFacts<'db> { + pub(super) module: Module<'db>, + pub(super) imports: nameres::ModuleImportSurface<'db>, + pub(super) item_resolutions: hir_nameres::ItemResolutionFacts<'db>, + pub(super) has_resolution_diagnostics: bool, + pub(super) instances: Vec>, +} + +/// Lowers every instance in a module once. +/// +/// The old environment path ran one tracked query per visible origin and the +/// soundness path independently repeated the same lowering and alias +/// normalization. A module query also makes imported std instances reusable +/// when many of their origins are visible together. +#[salsa::tracked(returns(ref))] +pub(super) fn module_instance_facts<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Option> { + let file = db.module_file(module)?; + let source = parse_file_to_hir(db, file).module(db); + let hir_module = crate::prepare_module(db, source).module(db); + let env = nameres::module_env_for_hir_module(db, module, hir_module); + let item_scope = env.item_scope.clone()?; + let resolution = + hir_nameres::resolve_item_types_with_imports(db, hir_module, &item_scope, &env); + let item_resolutions = resolution.facts(); + let instances = hir_module + .items(db) + .iter() + .filter_map(|item| match item { + Item::InstanceDef(instance) => Some(lower_instance_fact( + db, + hir_module, + *instance, + &item_resolutions, + )), + _ => None, + }) + .collect(); + + Some(InstanceModuleFacts { + module: hir_module, + imports: env.import_surface(), + item_resolutions, + has_resolution_diagnostics: !resolution.diagnostics.is_empty(), + instances, + }) +} + +fn lower_instance_fact<'db>( + db: &'db dyn Db, + module: Module<'db>, + instance: InstanceDef<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, +) -> InstanceFact<'db> { + let type_vars = type_var_bindings(instance.def_id_value(db), instance.type_var_elems(db)); + let lowerer = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let head_ref = instance.head(db); + let head = normalize_pred_aliases(db, module, item_resolutions, lowerer.lower_pred(head_ref)); + let conditions = instance + .preds(db) + .iter() + .map(|pred| { + let normalized = + normalize_pred_aliases(db, module, item_resolutions, lowerer.lower_pred(*pred)); + InstanceConditionFact { + pred: normalized.value, + span: LabelSpan::from_span(db, pred.span(db)), + alias_errors: normalized.errors, + } + }) + .collect(); + + InstanceFact { + instance, + def: instance.def_id_value(db), + binder_count: type_vars.len() as u32, + head: head.value, + head_span: LabelSpan::from_span(db, head_ref.span(db)), + head_alias_errors: head.errors, + conditions, + default: instance.default_kw(db).is_some(), + } +} diff --git a/crates/hir-ty/src/solver/match.rs b/crates/hir-ty/src/solver/match.rs new file mode 100644 index 00000000..0caf3e75 --- /dev/null +++ b/crates/hir-ty/src/solver/match.rs @@ -0,0 +1,751 @@ +use super::*; + +pub(super) fn max_pred_var<'db>(db: &'db dyn Db, pred: Pred<'db>) -> Option { + let mut max = None; + collect_max_pred_var(db, pred, &mut max); + max +} + +pub(super) fn offset_pred_vars<'db>(db: &'db dyn Db, pred: Pred<'db>, offset: u32) -> Pred<'db> { + match pred.kind(db) { + PredKind::InClass { class, main, args } => Pred::in_class( + db, + *class, + offset_ty_vars(db, *main, offset), + args.iter() + .map(|arg| offset_ty_vars(db, *arg, offset)) + .collect(), + ), + PredKind::Eq { lhs, rhs } => Pred::eq( + db, + offset_ty_vars(db, *lhs, offset), + offset_ty_vars(db, *rhs, offset), + ), + PredKind::Error => pred, + } +} + +fn offset_ty_vars<'db>(db: &'db dyn Db, ty: Ty<'db>, offset: u32) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => Ty::bound(db, var.index + offset), + TyKind::Named { ctor, args } => Ty::named( + db, + *ctor, + args.iter() + .map(|arg| offset_ty_vars(db, *arg, offset)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| offset_ty_vars(db, *param, offset)) + .collect(), + offset_ty_vars(db, *ret, offset), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| offset_ty_vars(db, *elem, offset)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, offset_ty_vars(db, *inner, offset)), + TyKind::Error | TyKind::Unknown => ty, + } +} + +#[derive(Clone, Default)] +pub(super) struct MatchSubst<'db> { + values: FxHashMap>, +} + +impl<'db> MatchSubst<'db> { + fn bind_flex(&mut self, db: &'db dyn Db, var: u32, ty: Ty<'db>) -> bool { + let ty = self.apply_ty(db, ty); + if matches!(ty.kind(db), TyKind::BoundVar(bound) if bound.index == var) { + return true; + } + if occurs_in_ty(db, var, ty) { + return false; + } + match self.values.get(&var).copied() { + Some(existing) => unify_ty(db, existing, ty, self, &FxHashSet::default()), + None => { + self.values.insert(var, ty); + true + } + } + } + + pub(super) fn merge(&mut self, db: &'db dyn Db, subst: &Substitution<'db>) -> bool { + for (var, ty) in &subst.values { + let ty = self.apply_ty(db, *ty); + match self.values.get(var).copied() { + Some(existing) if !ty_equal(db, self.apply_ty(db, existing), ty) => return false, + Some(_) => {} + None => { + self.values.insert(*var, ty); + } + } + } + true + } + + pub(super) fn apply_pred(&self, db: &'db dyn Db, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(db) { + PredKind::InClass { class, main, args } => Pred::in_class( + db, + *class, + self.apply_ty(db, *main), + args.iter().map(|arg| self.apply_ty(db, *arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => { + Pred::eq(db, self.apply_ty(db, *lhs), self.apply_ty(db, *rhs)) + } + PredKind::Error => Pred::error(db), + } + } + + pub(super) fn apply_ty(&self, db: &'db dyn Db, ty: Ty<'db>) -> Ty<'db> { + self.apply_ty_inner(db, ty, &mut FxHashSet::default()) + } + + fn apply_ty_inner( + &self, + db: &'db dyn Db, + ty: Ty<'db>, + visiting: &mut FxHashSet, + ) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => { + let Some(value) = self.values.get(&var.index).copied() else { + return ty; + }; + if !visiting.insert(var.index) { + return ty; + } + let value = self.apply_ty_inner(db, value, visiting); + visiting.remove(&var.index); + value + } + TyKind::Named { ctor, args } => Ty::named( + db, + *ctor, + args.iter() + .map(|arg| self.apply_ty_inner(db, *arg, visiting)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| self.apply_ty_inner(db, *param, visiting)) + .collect(), + self.apply_ty_inner(db, *ret, visiting), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| self.apply_ty_inner(db, *elem, visiting)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, self.apply_ty_inner(db, *inner, visiting)), + TyKind::Error | TyKind::Unknown => ty, + } + } + + pub(super) fn args_for_vars(&self, db: &'db dyn Db, vars: &[u32]) -> Vec> { + vars.iter() + .map(|index| self.apply_ty(db, Ty::bound(db, *index))) + .collect() + } + + pub(super) fn snapshot_for_vars(&self, db: &'db dyn Db, flex_count: u32) -> Substitution<'db> { + let mut values = Vec::new(); + for index in 0..flex_count { + let value = self.apply_ty(db, Ty::bound(db, index)); + if !matches!(value.kind(db), TyKind::BoundVar(var) if var.index == index) { + values.push((index, value)); + } + } + Substitution { values } + } +} + +#[derive(Clone)] +pub(super) struct InstantiatedClause<'db> { + pub(super) head: Pred<'db>, + pub(super) conditions: Vec>, + pub(super) origin: ClauseOrigin<'db>, + pub(super) binder_vars: Vec, +} + +pub(super) fn instantiate_clause<'db>( + db: &'db dyn Db, + clause: &ProgramClause<'db>, + goal: Pred<'db>, + avoid_vars: &FxHashSet, +) -> InstantiatedClause<'db> { + let base = next_var_index_for_clause(db, clause, goal, avoid_vars); + let mut rewriter = ClauseInstantiator { + db, + binder_count: clause.binder_count, + base, + }; + InstantiatedClause { + head: rewriter.pred(clause.head), + conditions: clause + .conditions + .iter() + .map(|condition| rewriter.pred(*condition)) + .collect(), + origin: clause.origin.clone(), + binder_vars: (0..clause.binder_count).map(|index| base + index).collect(), + } +} + +struct ClauseInstantiator<'db> { + db: &'db dyn Db, + binder_count: u32, + base: u32, +} + +impl<'db> ClauseInstantiator<'db> { + fn pred(&mut self, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(self.db) { + PredKind::InClass { class, main, args } => Pred::in_class( + self.db, + *class, + self.ty(*main), + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => Pred::eq(self.db, self.ty(*lhs), self.ty(*rhs)), + PredKind::Error => Pred::error(self.db), + } + } + + fn ty(&mut self, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(self.db) { + TyKind::BoundVar(var) if var.index < self.binder_count => { + Ty::bound(self.db, self.base + var.index) + } + TyKind::Named { ctor, args } => Ty::named( + self.db, + *ctor, + args.iter().map(|arg| self.ty(*arg)).collect(), + ), + TyKind::Function { params, ret } => Ty::function( + self.db, + params.iter().map(|param| self.ty(*param)).collect(), + self.ty(*ret), + ), + TyKind::Tuple(elems) => { + Ty::tuple(self.db, elems.iter().map(|elem| self.ty(*elem)).collect()) + } + TyKind::Comptime(inner) => Ty::comptime(self.db, self.ty(*inner)), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => ty, + } + } +} + +fn next_var_index_for_clause<'db>( + db: &'db dyn Db, + clause: &ProgramClause<'db>, + goal: Pred<'db>, + avoid_vars: &FxHashSet, +) -> u32 { + let mut max = None; + for var in avoid_vars { + max = Some(max.map_or(*var, |current: u32| current.max(*var))); + } + collect_max_pred_var(db, goal, &mut max); + collect_max_pred_var(db, clause.head, &mut max); + for condition in &clause.conditions { + collect_max_pred_var(db, *condition, &mut max); + } + max.map_or(0, |index| index + 1) +} + +pub(super) fn match_head<'db>( + db: &'db dyn Db, + pattern: Pred<'db>, + goal: Pred<'db>, + pattern_vars: &[u32], + goal_vars: &FxHashSet, +) -> Option> { + let mut subst = MatchSubst::default(); + let pattern_vars = pattern_vars.iter().copied().collect::>(); + if match_pred(db, pattern, goal, &mut subst, &pattern_vars, goal_vars) { + Some(subst) + } else { + None + } +} + +fn match_pred<'db>( + db: &'db dyn Db, + pattern: Pred<'db>, + goal: Pred<'db>, + subst: &mut MatchSubst<'db>, + pattern_vars: &FxHashSet, + goal_vars: &FxHashSet, +) -> bool { + match (pattern.kind(db), goal.kind(db)) { + ( + PredKind::InClass { + class: pattern_class, + main: pattern_main, + args: pattern_args, + }, + PredKind::InClass { + class: goal_class, + main: goal_main, + args: goal_args, + }, + ) if pattern_class == goal_class && pattern_args.len() == goal_args.len() => { + let mut weak_vars = pattern_vars.clone(); + weak_vars.extend(goal_vars.iter().copied()); + match_ty(db, *pattern_main, *goal_main, subst, pattern_vars) + && pattern_args + .iter() + .zip(goal_args) + .all(|(pattern_arg, goal_arg)| { + unify_ty(db, *pattern_arg, *goal_arg, subst, &weak_vars) + }) + } + ( + PredKind::Eq { + lhs: lhs1, + rhs: rhs1, + }, + PredKind::Eq { + lhs: lhs2, + rhs: rhs2, + }, + ) => { + let mut weak_vars = pattern_vars.clone(); + weak_vars.extend(goal_vars.iter().copied()); + unify_ty(db, *lhs1, *lhs2, subst, &weak_vars) + && unify_ty(db, *rhs1, *rhs2, subst, &weak_vars) + } + (PredKind::Error, PredKind::Error) => true, + _ => false, + } +} + +fn match_ty<'db>( + db: &'db dyn Db, + pattern: Ty<'db>, + goal: Ty<'db>, + subst: &mut MatchSubst<'db>, + pattern_vars: &FxHashSet, +) -> bool { + let pattern = subst.apply_ty(db, pattern); + let goal = subst.apply_ty(db, goal); + match pattern.kind(db) { + TyKind::BoundVar(var) if pattern_vars.contains(&var.index) => { + subst.bind_flex(db, var.index, goal) + } + TyKind::BoundVar(_) => ty_equal(db, pattern, goal), + TyKind::Error => matches!(goal.kind(db), TyKind::Error), + TyKind::Unknown => matches!(goal.kind(db), TyKind::Unknown), + TyKind::Named { + ctor: pattern_ctor, + args: pattern_args, + } => match goal.kind(db) { + TyKind::Named { + ctor: goal_ctor, + args: goal_args, + } if pattern_ctor == goal_ctor && pattern_args.len() == goal_args.len() => pattern_args + .iter() + .zip(goal_args) + .all(|(pattern_arg, goal_arg)| { + match_ty(db, *pattern_arg, *goal_arg, subst, pattern_vars) + }), + TyKind::Tuple(elems) + if matches!(pattern_ctor, TyCtor::Builtin(crate::BuiltinTyCtor::Unit)) + && pattern_args.is_empty() + && elems.is_empty() => + { + true + } + TyKind::Comptime(goal_inner) => match_ty(db, pattern, *goal_inner, subst, pattern_vars), + _ => false, + }, + TyKind::Function { + params: pattern_params, + ret: pattern_ret, + } => match goal.kind(db) { + TyKind::Function { + params: goal_params, + ret: goal_ret, + } if pattern_params.len() == goal_params.len() => { + pattern_params + .iter() + .zip(goal_params) + .all(|(pattern_param, goal_param)| { + match_ty(db, *pattern_param, *goal_param, subst, pattern_vars) + }) + && match_ty(db, *pattern_ret, *goal_ret, subst, pattern_vars) + } + TyKind::Comptime(goal_inner) => match_ty(db, pattern, *goal_inner, subst, pattern_vars), + _ => false, + }, + TyKind::Tuple(pattern_elems) => match goal.kind(db) { + TyKind::Tuple(goal_elems) if pattern_elems.len() == goal_elems.len() => pattern_elems + .iter() + .zip(goal_elems) + .all(|(pattern_elem, goal_elem)| { + match_ty(db, *pattern_elem, *goal_elem, subst, pattern_vars) + }), + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + } if pattern_elems.is_empty() && args.is_empty() => true, + TyKind::Comptime(goal_inner) => match_ty(db, pattern, *goal_inner, subst, pattern_vars), + _ => false, + }, + TyKind::Comptime(pattern_inner) => match goal.kind(db) { + TyKind::Comptime(goal_inner) => { + match_ty(db, *pattern_inner, *goal_inner, subst, pattern_vars) + } + _ => match_ty(db, *pattern_inner, goal, subst, pattern_vars), + }, + } +} + +pub(super) fn head_can_unify<'db>( + db: &'db dyn Db, + clause: &ProgramClause<'db>, + goal: Pred<'db>, + goal_vars: &FxHashSet, +) -> bool { + let instantiated = instantiate_clause(db, clause, goal, goal_vars); + let mut bindable = instantiated + .binder_vars + .iter() + .copied() + .collect::>(); + bindable.extend(goal_vars.iter().copied()); + let mut subst = MatchSubst::default(); + unify_pred(db, instantiated.head, goal, &mut subst, &bindable) +} + +fn unify_pred<'db>( + db: &'db dyn Db, + lhs: Pred<'db>, + rhs: Pred<'db>, + subst: &mut MatchSubst<'db>, + bindable: &FxHashSet, +) -> bool { + match (lhs.kind(db), rhs.kind(db)) { + ( + PredKind::InClass { + class: lhs_class, + main: lhs_main, + args: lhs_args, + }, + PredKind::InClass { + class: rhs_class, + main: rhs_main, + args: rhs_args, + }, + ) if lhs_class == rhs_class && lhs_args.len() == rhs_args.len() => { + unify_ty(db, *lhs_main, *rhs_main, subst, bindable) + && lhs_args + .iter() + .zip(rhs_args) + .all(|(lhs_arg, rhs_arg)| unify_ty(db, *lhs_arg, *rhs_arg, subst, bindable)) + } + ( + PredKind::Eq { + lhs: lhs_l, + rhs: lhs_r, + }, + PredKind::Eq { + lhs: rhs_l, + rhs: rhs_r, + }, + ) => { + unify_ty(db, *lhs_l, *rhs_l, subst, bindable) + && unify_ty(db, *lhs_r, *rhs_r, subst, bindable) + } + (PredKind::Error, PredKind::Error) => true, + _ => false, + } +} + +pub(super) fn unify_ty<'db>( + db: &'db dyn Db, + lhs: Ty<'db>, + rhs: Ty<'db>, + subst: &mut MatchSubst<'db>, + bindable: &FxHashSet, +) -> bool { + let lhs = subst.apply_ty(db, lhs); + let rhs = subst.apply_ty(db, rhs); + match (lhs.kind(db), rhs.kind(db)) { + (TyKind::BoundVar(lhs_var), _) if bindable.contains(&lhs_var.index) => { + subst.bind_flex(db, lhs_var.index, rhs) + } + (_, TyKind::BoundVar(rhs_var)) if bindable.contains(&rhs_var.index) => { + subst.bind_flex(db, rhs_var.index, lhs) + } + (TyKind::Error, TyKind::Error) | (TyKind::Unknown, TyKind::Unknown) => true, + (TyKind::BoundVar(lhs_var), TyKind::BoundVar(rhs_var)) => lhs_var == rhs_var, + ( + TyKind::Named { + ctor: lhs_ctor, + args: lhs_args, + }, + TyKind::Named { + ctor: rhs_ctor, + args: rhs_args, + }, + ) if lhs_ctor == rhs_ctor && lhs_args.len() == rhs_args.len() => lhs_args + .iter() + .zip(rhs_args) + .all(|(lhs_arg, rhs_arg)| unify_ty(db, *lhs_arg, *rhs_arg, subst, bindable)), + ( + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + TyKind::Tuple(elems), + ) + | ( + TyKind::Tuple(elems), + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + ) if args.is_empty() && elems.is_empty() => true, + ( + TyKind::Function { + params: lhs_params, + ret: lhs_ret, + }, + TyKind::Function { + params: rhs_params, + ret: rhs_ret, + }, + ) if lhs_params.len() == rhs_params.len() => { + lhs_params + .iter() + .zip(rhs_params) + .all(|(lhs_param, rhs_param)| unify_ty(db, *lhs_param, *rhs_param, subst, bindable)) + && unify_ty(db, *lhs_ret, *rhs_ret, subst, bindable) + } + (TyKind::Tuple(lhs_elems), TyKind::Tuple(rhs_elems)) + if lhs_elems.len() == rhs_elems.len() => + { + lhs_elems + .iter() + .zip(rhs_elems) + .all(|(lhs_elem, rhs_elem)| unify_ty(db, *lhs_elem, *rhs_elem, subst, bindable)) + } + (TyKind::Comptime(lhs_inner), TyKind::Comptime(rhs_inner)) => { + unify_ty(db, *lhs_inner, *rhs_inner, subst, bindable) + } + (TyKind::Comptime(lhs_inner), _) => unify_ty(db, *lhs_inner, rhs, subst, bindable), + (_, TyKind::Comptime(rhs_inner)) => unify_ty(db, lhs, *rhs_inner, subst, bindable), + _ => false, + } +} + +pub(super) fn ty_equal<'db>(db: &'db dyn Db, lhs: Ty<'db>, rhs: Ty<'db>) -> bool { + match (lhs.kind(db), rhs.kind(db)) { + (TyKind::Error, TyKind::Error) | (TyKind::Unknown, TyKind::Unknown) => true, + (TyKind::BoundVar(lhs), TyKind::BoundVar(rhs)) => lhs == rhs, + ( + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + TyKind::Tuple(elems), + ) + | ( + TyKind::Tuple(elems), + TyKind::Named { + ctor: TyCtor::Builtin(crate::BuiltinTyCtor::Unit), + args, + }, + ) if args.is_empty() && elems.is_empty() => true, + ( + TyKind::Named { + ctor: lhs_ctor, + args: lhs_args, + }, + TyKind::Named { + ctor: rhs_ctor, + args: rhs_args, + }, + ) => { + lhs_ctor == rhs_ctor + && lhs_args.len() == rhs_args.len() + && lhs_args + .iter() + .zip(rhs_args) + .all(|(lhs_arg, rhs_arg)| ty_equal(db, *lhs_arg, *rhs_arg)) + } + ( + TyKind::Function { + params: lhs_params, + ret: lhs_ret, + }, + TyKind::Function { + params: rhs_params, + ret: rhs_ret, + }, + ) => { + lhs_params.len() == rhs_params.len() + && lhs_params + .iter() + .zip(rhs_params) + .all(|(lhs_param, rhs_param)| ty_equal(db, *lhs_param, *rhs_param)) + && ty_equal(db, *lhs_ret, *rhs_ret) + } + (TyKind::Tuple(lhs), TyKind::Tuple(rhs)) => { + lhs.len() == rhs.len() + && lhs + .iter() + .zip(rhs) + .all(|(lhs_elem, rhs_elem)| ty_equal(db, *lhs_elem, *rhs_elem)) + } + (TyKind::Comptime(lhs), TyKind::Comptime(rhs)) => ty_equal(db, *lhs, *rhs), + (TyKind::Comptime(lhs), _) => ty_equal(db, *lhs, rhs), + (_, TyKind::Comptime(rhs)) => ty_equal(db, lhs, *rhs), + _ => false, + } +} + +fn occurs_in_ty<'db>(db: &'db dyn Db, var: u32, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::BoundVar(bound) => bound.index == var, + TyKind::Named { args, .. } => args.iter().any(|arg| occurs_in_ty(db, var, *arg)), + TyKind::Function { params, ret } => { + params.iter().any(|param| occurs_in_ty(db, var, *param)) || occurs_in_ty(db, var, *ret) + } + TyKind::Tuple(elems) => elems.iter().any(|elem| occurs_in_ty(db, var, *elem)), + TyKind::Comptime(inner) => occurs_in_ty(db, var, *inner), + TyKind::Error | TyKind::Unknown => false, + } +} + +pub(crate) fn collect_pred_vars<'db>(db: &'db dyn Db, pred: Pred<'db>, vars: &mut FxHashSet) { + match pred.kind(db) { + PredKind::InClass { main, args, .. } => { + collect_ty_vars(db, *main, vars); + for arg in args { + collect_ty_vars(db, *arg, vars); + } + } + PredKind::Eq { lhs, rhs } => { + collect_ty_vars(db, *lhs, vars); + collect_ty_vars(db, *rhs, vars); + } + PredKind::Error => {} + } +} + +pub(crate) fn collect_evidence_vars<'db>( + db: &'db dyn Db, + evidence: &Evidence<'db>, + vars: &mut FxHashSet, +) { + match evidence { + Evidence::Instance { + args, sub_evidence, .. + } => { + for arg in args { + collect_ty_vars(db, *arg, vars); + } + for evidence in sub_evidence { + collect_evidence_vars(db, evidence, vars); + } + } + Evidence::Builtin { pred } => collect_pred_vars(db, *pred, vars), + Evidence::Superclass { pred, child, .. } => { + collect_pred_vars(db, *pred, vars); + collect_evidence_vars(db, child, vars); + } + Evidence::Derived { + pred, sub_evidence, .. + } => { + collect_pred_vars(db, *pred, vars); + for evidence in sub_evidence { + collect_evidence_vars(db, evidence, vars); + } + } + } +} + +pub(crate) fn collect_ty_vars<'db>(db: &'db dyn Db, ty: Ty<'db>, vars: &mut FxHashSet) { + match ty.kind(db) { + TyKind::BoundVar(var) => { + vars.insert(var.index); + } + TyKind::Named { args, .. } => { + for arg in args { + collect_ty_vars(db, *arg, vars); + } + } + TyKind::Function { params, ret } => { + for param in params { + collect_ty_vars(db, *param, vars); + } + collect_ty_vars(db, *ret, vars); + } + TyKind::Tuple(elems) => { + for elem in elems { + collect_ty_vars(db, *elem, vars); + } + } + TyKind::Comptime(inner) => collect_ty_vars(db, *inner, vars), + TyKind::Error | TyKind::Unknown => {} + } +} + +fn collect_max_pred_var<'db>(db: &'db dyn Db, pred: Pred<'db>, max: &mut Option) { + match pred.kind(db) { + PredKind::InClass { main, args, .. } => { + collect_max_ty_var(db, *main, max); + for arg in args { + collect_max_ty_var(db, *arg, max); + } + } + PredKind::Eq { lhs, rhs } => { + collect_max_ty_var(db, *lhs, max); + collect_max_ty_var(db, *rhs, max); + } + PredKind::Error => {} + } +} + +fn collect_max_ty_var<'db>(db: &'db dyn Db, ty: Ty<'db>, max: &mut Option) { + match ty.kind(db) { + TyKind::BoundVar(var) => { + *max = Some(max.map_or(var.index, |current| current.max(var.index))); + } + TyKind::Named { args, .. } => { + for arg in args { + collect_max_ty_var(db, *arg, max); + } + } + TyKind::Function { params, ret } => { + for param in params { + collect_max_ty_var(db, *param, max); + } + collect_max_ty_var(db, *ret, max); + } + TyKind::Tuple(elems) => { + for elem in elems { + collect_max_ty_var(db, *elem, max); + } + } + TyKind::Comptime(inner) => collect_max_ty_var(db, *inner, max), + TyKind::Error | TyKind::Unknown => {} + } +} diff --git a/crates/hir-ty/src/solver/mod.rs b/crates/hir-ty/src/solver/mod.rs new file mode 100644 index 00000000..b97c2bf6 --- /dev/null +++ b/crates/hir-ty/src/solver/mod.rs @@ -0,0 +1,501 @@ +//! Tabled type-class resolution. +//! +//! Class and instance declarations are lowered into Horn-style `ProgramClause`s +//! (`head :- conditions`) and interned into a per-module `TraitEnvId`. A class +//! goal is canonicalized (`canonicalize_goal`) and discharged by the shared +//! `tablesolve` proof-forest engine through Solcore's `TabledEngine` adapter. +//! +//! Solcore owns the language-specific operations exposed to `tablesolve`: +//! canonicalization, clause selection and matching, answer rebasing, evidence +//! construction, and answer identity. The library owns table allocation, +//! generator/consumer scheduling, duplicate suppression, and fixpoint +//! detection. Local givens, instances, superclass projections, and fallback +//! defaults retain their existing resolution order. +//! +//! Because every subgoal is solved once and shared, diamond-shaped constraint +//! graphs are resolved without the exponential blow-up of naive backtracking, +//! and cyclic instance dependencies saturate instead of diverging: re-entering +//! an in-progress subgoal only registers another consumer on its existing table +//! entry. A `DEFAULT_SOLVER_FUEL` bound is retained purely as a backstop for +//! constraint spaces that keep generating strictly larger types (which tabling +//! alone does not bound); cyclic and diamond goals terminate without consuming +//! it to exhaustion. +//! +//! The tabling strategy follows Selsam, Ullrich & de Moura, "Tabled Typeclass +//! Resolution" (). +//! +//! Instance soundness (the coverage, Patterson, and bounded-variable +//! conditions) is checked separately by the module-level +//! `instance_soundness_diagnostics` query and does not affect the answers the +//! engine returns. + +use std::collections::VecDeque; + +use hir::{ + Db as HirDb, + anchor::DefId, + ast::{ + Ident, + function::{FuncParam, FuncSig}, + item::{AdtDef, ClassDef, ContractItem, FunctionDef, InstanceDef, Item, Module}, + }, + diag::LabelSpan, + nameres as hir_nameres, + span::{Spanned, SpannedElem}, +}; +use nameres::ModuleId; +use parser::{parse_diagnostics, parse_file_to_hir}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + BinderEnv, BuiltinClassId, ClassId, Db, Pred, PredKind, Ty, TyCtor, TyKind, TyScheme, + TypeLowering, TypeckDiagnostic, + alias::{AliasError, AliasNormalizer, normalize_pred_aliases}, + class_method_type_vars, +}; + +const DEFAULT_SOLVER_FUEL: usize = 16_384; + +mod canonical; +mod derived_generic; +mod display; +mod engine; +mod env; +mod evidence; +mod instance_facts; +mod r#match; +mod module_lookup; +mod soundness; + +use canonical::{ + GoalRenaming, RigidVar, TableKey, actualize_answer, canonicalize_goal, canonicalize_local_given, +}; +pub use derived_generic::{ + derived_generic_instance_plan, derived_generic_plan, generic_derivation_diagnostics, +}; +use derived_generic::{ + derived_generic_instance_plan_with_resolutions, imported_generic_class, local_adt_infos, + local_generic_class, visible_generic_class, +}; +use display::{display_scheme_source, display_vars}; +use engine::{Answer, TabledEngine}; +pub use env::{ + trait_env_for_module, trait_env_from_module_resolution, + trait_env_from_module_resolution_and_imports, trait_env_with_givens, +}; +use evidence::{apply_evidence, clause_evidence, solution_from_answers}; +use instance_facts::{InstanceFact, module_instance_facts}; +use r#match::{ + InstantiatedClause, MatchSubst, head_can_unify, instantiate_clause, match_head, max_pred_var, + offset_pred_vars, ty_equal, unify_ty, +}; +pub(crate) use r#match::{collect_evidence_vars, collect_pred_vars, collect_ty_vars}; +use module_lookup::{ + ident_text, module_for_def, scope_resolution_for_module_id, type_var_bindings, unique_modules, + unique_preds, visible_class_modules, +}; +pub use soundness::instance_soundness_diagnostics; + +use crate::display::{display_class_source, display_pred_source, display_ty_source}; + +#[salsa::interned(debug)] +pub struct CanonicalGoal<'db> { + /// Canonical class predicate. + pub pred: Pred<'db>, + /// Goal variables that may be solved by instance matching. + #[returns(ref)] + pub allowed_vars: Vec, +} + +/// Interned deterministic subset of trait solver clauses. +#[salsa::interned(debug)] +pub struct TraitClauseSetId<'db> { + /// Clauses in their local resolution order. + #[returns(ref)] + pub clauses: Vec>, +} + +/// Stable sources that define a module-backed trait environment. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleTraitEnvSource<'db> { + /// Modules whose visible class definitions contribute superclass clauses. + pub superclass_modules: Vec>, + /// Visible instance origins, in resolution order. + pub instance_origins: Vec>, + /// Local source for derived `Generic` clauses, when `Generic` is visible. + pub derived_generic: Option>, +} + +/// Stable source of synthesized `Generic` clauses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct DerivedGenericClauseSource<'db> { + /// Module whose local ADTs may receive synthesized `Generic` clauses. + pub module: ModuleId<'db>, + /// Visible `Generic` class definition. + pub generic: DefId<'db>, +} + +/// Source layout for a base trait environment. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum BaseTraitEnvSource<'db> { + /// File-backed module environment. Clause contents are queried from these + /// stable sources so edits to one origin do not churn the whole env key. + Module(ModuleTraitEnvSource<'db>), + /// Ad-hoc environment built from an already resolved HIR module. + Resolved { + /// Clause subsets in final solver concatenation order. + clause_sets: Vec>, + }, +} + +/// Interned base trait environment for one module. +#[salsa::interned(debug)] +pub struct BaseTraitEnvId<'db> { + /// Stable source description for visible builtin, superclass, instance, + /// and synthesized clauses. + #[returns(ref)] + pub source: BaseTraitEnvSource<'db>, +} + +/// Interned local assumptions layered on top of a base trait environment. +#[salsa::interned(debug)] +pub struct LocalGivensId<'db> { + /// Local assumptions available while checking a polymorphic body. + #[returns(ref)] + pub preds: Vec>, +} + +/// Interned trait environment for one solving context. +#[salsa::interned(debug)] +pub struct TraitEnvId<'db> { + /// Module-level instance, superclass, and builtin clauses. + pub base: BaseTraitEnvId<'db>, + /// Local assumptions available while checking a polymorphic body. + pub givens: LocalGivensId<'db>, +} + +/// One type-class program clause: `head :- conditions`. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ProgramClause<'db> { + /// Number of de Bruijn binders in scope for this clause. + pub binder_count: u32, + /// Clause head. + pub head: Pred<'db>, + /// Clause body predicates. + pub conditions: Vec>, + /// Evidence constructor produced by this clause. + pub origin: ClauseOrigin<'db>, +} + +/// Source of a program clause. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ClauseOrigin<'db> { + /// User-defined instance declaration. + Instance { def: DefId<'db>, default: bool }, + /// Compiler-defined fact. + Builtin, + /// Compiler-synthesized instance-like clause. + Derived(DerivedClauseKind<'db>), + /// Local given predicate from a checked body. + Given, + /// Superclass projection clause. + Superclass(DefId<'db>), +} + +impl<'db> ClauseOrigin<'db> { + pub(crate) fn is_default(&self) -> bool { + matches!(self, ClauseOrigin::Instance { default: true, .. }) + } +} + +/// Family of compiler-synthesized clauses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum DerivedClauseKind<'db> { + /// Automatically derived `Generic` instance. + Generic { + /// ADT whose `Generic` instance was synthesized. + adt: DefId<'db>, + }, + /// Lambda closure `invokable` instance. + Closure, +} + +/// Queryable plan for an automatically derived `Generic` instance. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DerivedGenericPlan<'db> { + /// ADT whose instance is synthesized. + pub adt: DefId<'db>, + /// SOP representation type used by `Generic(rep)`. + pub rep: Ty<'db>, + /// Match arms for the synthesized `Generic.from` method. + pub from_arms: Vec>, + /// Match arms for the synthesized `Generic.to` method. + pub to_arms: Vec>, +} + +/// One constructor arm in a synthesized `Generic.from` body. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DerivedGenericFromArm<'db> { + /// Constructor ordinal in source declaration order. + pub ctor_index: u32, + /// Number of fields in the source constructor parameter list. + pub field_count: u32, + /// Constructor name. + pub ctor_name: String, + /// Product payload representation before sum wrapping. + pub product_rep: Ty<'db>, + /// Number of `inr` wrappers before this case. + pub inr_depth: u32, + /// Whether this non-final case is wrapped in `inl`. + pub wraps_inl: bool, +} + +/// One representation arm in a synthesized `Generic.to` body. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct DerivedGenericToArm<'db> { + /// Constructor ordinal in source declaration order. + pub ctor_index: u32, + /// Number of fields in the source constructor parameter list. + pub field_count: u32, + /// Constructor name. + pub ctor_name: String, + /// Product payload representation after sum unwrapping. + pub product_rep: Ty<'db>, + /// Number of `inr` pattern wrappers before this case. + pub inr_depth: u32, + /// Whether this non-final case is matched through `inl`. + pub wraps_inl: bool, +} + +/// Lifetime-free evidence tree for a solved obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum Evidence<'db> { + /// Evidence built by selecting an instance and recursively solving its + /// context predicates. + Instance { + /// Selected instance definition. + instance: DefId<'db>, + /// Clause type arguments after matching the goal. + args: Vec>, + /// Evidence for instance context predicates. + sub_evidence: Vec>, + }, + /// Builtin or assumed evidence with no instance body. + Builtin { + /// Predicate discharged directly. + pred: Pred<'db>, + }, + /// Evidence obtained by projecting a superclass dictionary from evidence + /// for the subclass. + Superclass { + /// Class declaration that introduced the superclass relationship. + class: DefId<'db>, + /// Predicate discharged by the projection. + pred: Pred<'db>, + /// Evidence for the subclass predicate. + child: Box>, + }, + /// Evidence from a compiler-synthesized clause. + Derived { + /// Derived clause family. + kind: DerivedClauseKind<'db>, + /// Predicate discharged directly. + pred: Pred<'db>, + /// Evidence for synthesized clause context predicates. + sub_evidence: Vec>, + }, +} + +/// Substitution snapshot attached to a solution candidate. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, salsa::Update)] +pub struct Substitution<'db> { + /// Clause variable assignments in binder-index order. + pub values: Vec<(u32, Ty<'db>)>, +} + +/// One possible proof candidate. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct Candidate<'db> { + /// Candidate substitution. + pub subst: Substitution<'db>, + /// Candidate evidence. + pub evidence: Evidence<'db>, +} + +/// Solver answer. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum Solution<'db> { + /// Exactly one proof exists. + Unique { + /// Canonical substitution. + subst: Substitution<'db>, + /// Evidence tree. + evidence: Evidence<'db>, + }, + /// More than one non-overlapping proof candidate exists. + Ambiguous { + /// Competing candidates. + candidates: Vec>, + }, + /// No proof exists. + NoSolution, +} + +/// Internal solver report used to surface fuel exhaustion. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct SolverReport<'db> { + /// Solver answer. + pub solution: Solution<'db>, + /// Whether the solver exhausted its fuel before proving the goal. + pub exhausted: bool, + /// Fuel remaining after the top-level solve finished. + pub fuel_remaining: usize, + /// Tabled-engine counters, exposed for solver regression tests. + pub stats: SolverStats, +} + +/// Internal tabled-engine counters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, salsa::Update)] +pub struct SolverStats { + /// Number of table entries allocated during this solve. + pub table_size: usize, + /// Number of generator clause attempts. + pub generator_steps: usize, + /// Number of fresh answers admitted to tables. + pub answers_found: usize, +} + +/// Wraps a predicate as a solver goal. +pub fn canonical_goal<'db>(db: &'db dyn Db, pred: Pred<'db>) -> CanonicalGoal<'db> { + CanonicalGoal::new(db, pred, Vec::new()) +} + +/// Wraps a predicate as a solver goal with bindable goal variables. +pub fn canonical_goal_with_allowed<'db>( + db: &'db dyn Db, + pred: Pred<'db>, + mut allowed_vars: Vec, +) -> CanonicalGoal<'db> { + allowed_vars.sort_unstable(); + allowed_vars.dedup(); + CanonicalGoal::new(db, pred, allowed_vars) +} + +#[salsa::tracked] +pub fn solve<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + goal: CanonicalGoal<'db>, +) -> Solution<'db> { + solve_report(db, env, goal).solution +} + +/// Tracked solver query that includes fuel exhaustion details. +#[salsa::tracked] +pub fn solve_report<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + goal: CanonicalGoal<'db>, +) -> SolverReport<'db> { + solve_goal(db, env, goal.pred(db), goal.allowed_vars(db)) +} + +fn solve_goal<'db>( + db: &'db dyn Db, + env: TraitEnvId<'db>, + goal: Pred<'db>, + allowed_vars: &[u32], +) -> SolverReport<'db> { + let mut solver = Solver::new(db, env, DEFAULT_SOLVER_FUEL); + let allowed_vars = allowed_vars.iter().copied().collect(); + let mut report = solver.solve_pred_with_allowed(goal, &allowed_vars); + report.fuel_remaining = solver.fuel; + report.stats = solver.stats; + report +} + +impl<'db> SolverReport<'db> { + fn new(solution: Solution<'db>, exhausted: bool) -> Self { + Self { + solution, + exhausted, + fuel_remaining: 0, + stats: SolverStats::default(), + } + } +} + +impl<'db> TraitEnvId<'db> { + /// Returns the base program clauses visible to this environment. + pub fn clauses(self, db: &'db dyn Db) -> Vec> { + self.clauses_ref(db).clone() + } + + fn clauses_ref(self, db: &'db dyn Db) -> &'db Vec> { + env::base_trait_env_clauses(db, self.base(db)) + } + + /// Returns the base clauses relevant to the goal's class family. + fn clauses_for_pred(self, db: &'db dyn Db, pred: Pred<'db>) -> &'db Vec> { + match pred.kind(db) { + PredKind::InClass { class, .. } => { + env::base_trait_env_class_clauses(db, self.base(db), *class) + } + PredKind::Eq { .. } | PredKind::Error => self.clauses_ref(db), + } + } + + /// Returns local given predicates layered over the base environment. + pub fn local_givens(self, db: &'db dyn Db) -> &'db Vec> { + self.givens(db).preds(db) + } +} + +struct Solver<'db> { + db: &'db dyn Db, + env: TraitEnvId<'db>, + fuel: usize, + stats: SolverStats, +} + +impl<'db> Solver<'db> { + fn new(db: &'db dyn Db, env: TraitEnvId<'db>, fuel: usize) -> Self { + Self { + db, + env, + fuel, + stats: SolverStats::default(), + } + } + + /// Solve `goal`, selecting defaults independently for each tabled subgoal. + /// The engine admits a default only when no non-default clause head can + /// unify with that particular subgoal, so defaults do not mask specific + /// instances but can still discharge conditions of a non-default parent. + fn solve_pred_with_allowed( + &mut self, + goal: Pred<'db>, + allowed_goal_vars: &FxHashSet, + ) -> SolverReport<'db> { + let mut engine = TabledEngine::new(self.db, self.env, self.fuel); + let result = engine.run(goal, allowed_goal_vars); + self.fuel = result.fuel_remaining; + self.stats.add(result.stats); + + let mut report = SolverReport::new( + solution_from_answers(self.db, self.env, result.answers), + result.exhausted, + ); + report.fuel_remaining = self.fuel; + report.stats = self.stats; + report + } +} + +impl SolverStats { + fn add(&mut self, other: Self) { + self.table_size += other.table_size; + self.generator_steps += other.generator_steps; + self.answers_found += other.answers_found; + } +} diff --git a/crates/hir-ty/src/solver/module_lookup.rs b/crates/hir-ty/src/solver/module_lookup.rs new file mode 100644 index 00000000..026c8023 --- /dev/null +++ b/crates/hir-ty/src/solver/module_lookup.rs @@ -0,0 +1,58 @@ +pub(super) use hir_nameres::{ident_text, type_var_bindings}; + +use super::*; +pub(super) use crate::support::module_for_def_via_tree as module_for_def; + +pub(super) fn visible_class_modules<'db>( + db: &'db dyn Db, + env: &nameres::ModuleImportSurface<'db>, +) -> Vec> { + env.types + .values() + .filter_map(|resolution| match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } => module_for_def(db, *def), + _ => None, + }) + .collect() +} + +pub(super) fn scope_resolution_for_module_id<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Option<( + hir_nameres::ItemScopeFacts<'db>, + hir_nameres::ItemResolutionFacts<'db>, +)> { + let env = nameres::module_import_surface(db, module); + let scope = env.item_scope.clone()?; + let item_resolutions = + hir_nameres::resolve_item_type_facts_with_imports(db, scope.module, &scope, &env); + Some((scope, item_resolutions)) +} + +pub(super) fn unique_modules<'db>( + values: impl IntoIterator>, +) -> Vec> { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value) { + result.push(value); + } + } + result +} + +pub(super) fn unique_preds<'db>(values: impl IntoIterator>) -> Vec> { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value) { + result.push(value); + } + } + result +} diff --git a/crates/hir-ty/src/solver/soundness.rs b/crates/hir-ty/src/solver/soundness.rs new file mode 100644 index 00000000..12d126af --- /dev/null +++ b/crates/hir-ty/src/solver/soundness.rs @@ -0,0 +1,893 @@ +use super::*; + +#[salsa::tracked(returns(ref))] +pub fn instance_soundness_diagnostics<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Vec { + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + if !parse_diagnostics(db, file).is_empty() { + return Vec::new(); + } + let source = parse_file_to_hir(db, file).module(db); + let hir_module = crate::prepare_module(db, source).module(db); + if !hir_module + .items(db) + .iter() + .any(|item| matches!(item, Item::InstanceDef(_))) + { + return Vec::new(); + } + let Some(facts) = module_instance_facts(db, module).as_ref() else { + return Vec::new(); + }; + if facts.has_resolution_diagnostics { + return Vec::new(); + } + + let pragmas = InstanceSoundnessPragmas::from_module(db, facts.module); + let mut diagnostics = + crate::alias::type_alias_normalization_errors(db, facts.module, &facts.item_resolutions) + .into_iter() + .map(alias_error_to_diagnostic) + .collect::>(); + let mut prior_heads = imported_non_default_heads(db, module, &facts.imports); + for fact in &facts.instances { + let class = fact.class(db); + let same_class_prior = class + .and_then(|class| prior_heads.get(&class)) + .map(Vec::as_slice) + .unwrap_or_default(); + if let Some(head) = check_instance_soundness( + db, + facts.module, + fact, + &facts.item_resolutions, + &pragmas, + same_class_prior, + &mut diagnostics, + ) && !fact.default + && let Some(class) = class + { + prior_heads.entry(class).or_default().push(InstanceHead { + pred: head, + span: fact.head_span.clone(), + }); + } + } + diagnostics +} + +#[derive(Clone)] +struct InstanceHead<'db> { + pred: Pred<'db>, + span: LabelSpan, +} + +#[derive(Default)] +struct InstanceSoundnessPragmas { + coverage: PragmaEscape, + patterson: PragmaEscape, + bounded_variable: PragmaEscape, +} + +#[derive(Default)] +struct PragmaEscape { + all: bool, + classes: FxHashSet, +} + +impl InstanceSoundnessPragmas { + fn from_module<'db>(db: &'db dyn Db, module: Module<'db>) -> Self { + let mut pragmas = Self::default(); + for item in module.items(db) { + let Item::Pragma(pragma) = item else { + continue; + }; + let name = (*pragma.name(db).atom()).text(db); + match name { + "noCoverageCondition" => { + pragmas.coverage.add_items(db, pragma.items(db)); + } + "noPattersonCondition" => { + pragmas.patterson.add_items(db, pragma.items(db)); + } + "noBoundVariableCondition" => { + pragmas.bounded_variable.add_items(db, pragma.items(db)); + } + _ => {} + } + } + pragmas + } +} + +impl PragmaEscape { + fn add_items<'db>(&mut self, db: &'db dyn Db, items: &[SpannedElem<'db, Ident<'db>>]) { + if items.is_empty() { + self.all = true; + return; + } + self.classes + .extend(items.iter().map(|item| (*item.atom()).text(db).to_owned())); + } + + fn disables(&self, class_name: &str) -> bool { + self.all || self.classes.contains(class_name) + } +} + +fn check_instance_soundness<'db>( + db: &'db dyn Db, + module: Module<'db>, + fact: &InstanceFact<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + pragmas: &InstanceSoundnessPragmas, + prior_heads: &[InstanceHead<'db>], + diagnostics: &mut Vec, +) -> Option> { + let instance = fact.instance; + let type_vars = type_var_bindings(instance.def_id_value(db), instance.type_var_elems(db)); + let type_var_names = type_var_names(db, &type_vars); + let head_ref = instance.head(db); + let head_span = fact.head_span.clone(); + let class_name = head_ref_class_name(db, head_ref); + diagnostics.extend( + fact.head_alias_errors + .iter() + .cloned() + .map(alias_error_to_diagnostic), + ); + let head = fact.head; + if matches!(head.kind(db), PredKind::Error) { + return None; + } + let conditions = fact + .conditions + .iter() + .map(|condition| { + diagnostics.extend( + condition + .alias_errors + .iter() + .cloned() + .map(alias_error_to_diagnostic), + ); + (condition.pred, condition.span.clone()) + }) + .collect::>(); + + check_pred_class_arity(db, module, head, head_span.clone(), diagnostics); + for (condition, span) in &conditions { + check_pred_class_arity(db, module, *condition, span.clone(), diagnostics); + } + check_default_instance_head( + db, + head, + head_span.clone(), + fact.default, + &type_var_names, + diagnostics, + ); + if !fact.default { + check_overlapping_instance( + db, + head, + head_span.clone(), + prior_heads, + &type_var_names, + diagnostics, + ); + } + check_instance_methods(db, module, instance, item_resolutions, head, diagnostics); + + if !pragmas.coverage.disables(&class_name) { + check_coverage_condition( + db, + head, + head_span.clone(), + &class_name, + &type_var_names, + diagnostics, + ); + } + if !pragmas.patterson.disables(&class_name) { + let condition_preds = conditions + .iter() + .map(|(condition, _)| *condition) + .collect::>(); + check_patterson_condition( + db, + head, + head_span.clone(), + &condition_preds, + &type_var_names, + diagnostics, + ); + } + if !pragmas.bounded_variable.disables(&class_name) { + let condition_preds = conditions + .iter() + .map(|(condition, _)| *condition) + .collect::>(); + check_bounded_variable_condition(db, head, head_span, &condition_preds, diagnostics); + } + Some(head) +} + +fn alias_error_to_diagnostic(error: AliasError) -> TypeckDiagnostic { + match error { + AliasError::Cycle { span, alias } => TypeckDiagnostic::TypeAliasCycle { span, alias }, + AliasError::Arity { + span, + alias, + expected, + actual, + } => TypeckDiagnostic::TypeAliasArity { + span, + alias, + expected, + actual, + }, + AliasError::ExpansionLimit { span, limit } => { + TypeckDiagnostic::TypeAliasExpansionLimit { span, limit } + } + } +} + +fn imported_non_default_heads<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + env: &nameres::ModuleImportSurface<'db>, +) -> FxHashMap, Vec>> { + let mut heads = FxHashMap::, Vec>>::default(); + for origin in &env.instances { + if origin.module == module { + continue; + } + let Some(facts) = module_instance_facts(db, origin.module).as_ref() else { + continue; + }; + let Some(fact) = facts + .instances + .iter() + .find(|fact| fact.def == origin.def_id) + else { + continue; + }; + if fact.default { + continue; + } + if let Some(class) = fact.class(db) { + heads.entry(class).or_default().push(InstanceHead { + pred: fact.head, + span: fact.head_span.clone(), + }); + } + } + heads +} + +fn check_pred_class_arity<'db>( + db: &'db dyn Db, + module: Module<'db>, + pred: Pred<'db>, + span: LabelSpan, + diagnostics: &mut Vec, +) { + let PredKind::InClass { class, args, .. } = pred.kind(db) else { + return; + }; + let Some(expected) = class_arity(db, module, *class) else { + return; + }; + if expected != args.len() { + diagnostics.push(TypeckDiagnostic::ClassArity { + span, + class: display_class_source(db, *class), + expected, + actual: args.len(), + }); + } +} + +fn class_arity<'db>(db: &'db dyn Db, module: Module<'db>, class: ClassId<'db>) -> Option { + match class { + ClassId::Builtin(BuiltinClassId::Invokable) => Some(2), + ClassId::Builtin(BuiltinClassId::Int) => Some(0), + ClassId::User(def) => { + let class_module = module_for_def(db, def) + .and_then(|module| scope_resolution_for_module_id(db, module).map(|it| it.0.module)) + .unwrap_or(module); + find_class_info(db, class_module, def) + .map(|info| info.class.head(db).kind(db).args.atom().len()) + } + } +} + +fn check_default_instance_head<'db>( + db: &'db dyn Db, + head: Pred<'db>, + span: LabelSpan, + is_default: bool, + type_var_names: &[String], + diagnostics: &mut Vec, +) { + if !is_default { + return; + } + let PredKind::InClass { main, .. } = head.kind(db) else { + diagnostics.push(TypeckDiagnostic::InvalidDefaultInstance { + span, + head: display_pred_source(db, head, type_var_names), + }); + return; + }; + if !ty_contains_bound_var(db, *main) { + diagnostics.push(TypeckDiagnostic::InvalidDefaultInstance { + span, + head: display_pred_source(db, head, type_var_names), + }); + } +} + +fn ty_contains_bound_var(db: &dyn Db, ty: Ty<'_>) -> bool { + match ty.kind(db) { + TyKind::BoundVar(_) => true, + TyKind::Named { args, .. } | TyKind::Tuple(args) => { + args.iter().any(|arg| ty_contains_bound_var(db, *arg)) + } + TyKind::Function { params, ret } => { + params.iter().any(|param| ty_contains_bound_var(db, *param)) + || ty_contains_bound_var(db, *ret) + } + TyKind::Comptime(inner) => ty_contains_bound_var(db, *inner), + TyKind::Error | TyKind::Unknown => false, + } +} + +fn check_overlapping_instance<'db>( + db: &'db dyn Db, + head: Pred<'db>, + head_span: LabelSpan, + prior_heads: &[InstanceHead<'db>], + type_var_names: &[String], + diagnostics: &mut Vec, +) { + for prior in prior_heads { + if instance_heads_overlap(db, head, prior.pred) { + diagnostics.push(TypeckDiagnostic::OverlappingInstance { + instance_span: head_span, + overlaps_span: Some(prior.span.clone()), + instance: display_pred_source(db, head, type_var_names), + overlaps: display_pred_source(db, prior.pred, &[]), + }); + return; + } + } +} + +fn instance_heads_overlap<'db>(db: &'db dyn Db, lhs: Pred<'db>, rhs: Pred<'db>) -> bool { + let offset = max_pred_var(db, lhs).map_or(0, |index| index + 1); + let rhs = offset_pred_vars(db, rhs, offset); + let mut bindable = FxHashSet::default(); + collect_pred_vars(db, lhs, &mut bindable); + collect_pred_vars(db, rhs, &mut bindable); + let mut subst = MatchSubst::default(); + match (lhs.kind(db), rhs.kind(db)) { + (PredKind::InClass { main: lhs_main, .. }, PredKind::InClass { main: rhs_main, .. }) => { + unify_ty(db, *lhs_main, *rhs_main, &mut subst, &bindable) + } + _ => false, + } +} + +fn check_instance_methods<'db>( + db: &'db dyn Db, + module: Module<'db>, + instance: InstanceDef<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + head: Pred<'db>, + diagnostics: &mut Vec, +) { + let PredKind::InClass { + class: ClassId::User(class_def), + .. + } = head.kind(db) + else { + return; + }; + let class_module = module_for_def(db, *class_def) + .and_then(|module| scope_resolution_for_module_id(db, module).map(|it| it.0.module)) + .unwrap_or(module); + let Some(class_info) = find_class_info(db, class_module, *class_def) else { + return; + }; + let class_name = class_info + .class + .def_id_value(db) + .name(db) + .unwrap_or_else(|| "".to_owned()); + let methods = instance.methods(db); + let method_names = methods + .iter() + .map(|method| ident_text(db, &method.sig(db).name)) + .collect::>(); + let required = class_info + .class + .methods(db) + .iter() + .map(|method| ident_text(db, &method.name)) + .collect::>(); + let missing = required + .iter() + .filter(|required| !method_names.iter().any(|name| name == *required)) + .cloned() + .collect::>(); + let extra = method_names + .iter() + .filter(|name| !required.iter().any(|required| required == *name)) + .collect::>(); + for extra in extra { + if let Some(method) = methods + .iter() + .find(|method| ident_text(db, &method.sig(db).name) == *extra) + { + diagnostics.push(TypeckDiagnostic::UnknownInstanceMethod { + span: LabelSpan::from_span(db, method.sig(db).name.span(db)), + name: format!("{class_name}.{extra}"), + class_span: Some(LabelSpan::from_span( + db, + class_info.class.head(db).kind(db).class.span(db), + )), + }); + } + } + if !missing.is_empty() { + diagnostics.push(TypeckDiagnostic::IncompleteInstance { + span: LabelSpan::from_span(db, instance.head(db).span(db)), + class: class_name.clone(), + missing, + }); + } + + for class_method in class_info.class.methods(db) { + let method_name = ident_text(db, &class_method.name); + let Some(instance_method) = methods + .iter() + .find(|method| ident_text(db, &method.sig(db).name) == method_name) + else { + continue; + }; + let ctx = InstanceMethodCheckCtx { + db, + module, + item_resolutions, + class_info: &class_info, + instance_head: head, + instance_head_span: LabelSpan::from_span(db, instance.head(db).span(db)), + }; + check_instance_method_signature(&ctx, class_method, *instance_method, diagnostics); + } +} + +struct InstanceMethodCheckCtx<'a, 'db> { + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &'a hir_nameres::ItemResolutionFacts<'db>, + class_info: &'a ClassLookup<'db>, + instance_head: Pred<'db>, + instance_head_span: LabelSpan, +} + +fn check_instance_method_signature<'db>( + ctx: &InstanceMethodCheckCtx<'_, 'db>, + class_method: &FuncSig<'db>, + instance_method: FunctionDef<'db>, + diagnostics: &mut Vec, +) { + let db = ctx.db; + let method_name = ident_text(db, &class_method.name); + if let Some(reason) = incomplete_class_method_signature_reason(class_method) { + diagnostics.push(TypeckDiagnostic::InvalidInstanceMethodSignature { + span: LabelSpan::from_span(db, class_method.span(db)), + method: method_name.clone(), + reason, + }); + return; + } + if let Some(reason) = incomplete_instance_method_signature_reason(instance_method.sig(db)) { + diagnostics.push(TypeckDiagnostic::InvalidInstanceMethodSignature { + span: LabelSpan::from_span(db, instance_method.sig(db).span(db)), + method: method_name.clone(), + reason, + }); + return; + } + + let class_method_type_vars = class_method_type_vars(db, ctx.class_info.class, class_method); + let class_lowerer = TypeLowering::from_item_resolutions( + db, + ctx.item_resolutions, + BinderEnv::from_type_vars(&class_method_type_vars), + ); + let mut class_normalizer = AliasNormalizer::new(db, ctx.module, ctx.item_resolutions); + let class_scheme = class_lowerer.lower_class_method(ctx.class_info.class, class_method); + let class_scheme = class_normalizer.normalize_scheme(class_scheme); + let class_head = + class_normalizer.normalize_pred(class_lowerer.lower_pred(ctx.class_info.class.head(db))); + diagnostics.extend( + class_normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic), + ); + + let mut subst = FxHashMap::default(); + if !bind_class_head_vars(db, class_head, ctx.instance_head, &mut subst) { + return; + } + + let mut method_type_vars = type_var_bindings( + instance_method.def_id_value(db), + &instance_method.sig(db).type_vars, + ); + let mut inherited = type_var_bindings_for_instance(db, instance_method, ctx.module); + let instance_binder_count = inherited.len() as u32; + let class_binder_count = ctx.class_info.type_vars.len() as u32; + for index in 0..class_method.type_vars.len() as u32 { + // Class-local method binders follow class-head binders in the class + // scheme. Rebase them onto the instance method's binder space after + // substituting the class head with the instance head. + subst.insert( + class_binder_count + index, + Ty::bound(db, instance_binder_count + index), + ); + } + let expected = substitute_bound_vars(db, class_scheme.body(db).ty(db), &subst); + + inherited.append(&mut method_type_vars); + let method_lowerer = TypeLowering::from_item_resolutions( + db, + ctx.item_resolutions, + BinderEnv::from_type_vars(&inherited), + ); + let mut actual_normalizer = AliasNormalizer::new(db, ctx.module, ctx.item_resolutions); + let actual_scheme = + actual_normalizer.normalize_scheme(method_lowerer.lower_function(instance_method).scheme); + if scheme_is_ambiguous(db, actual_scheme) { + diagnostics.push(TypeckDiagnostic::AmbiguousInferredType { + span: ctx.instance_head_span.clone(), + scheme: display_scheme_source(db, actual_scheme, &inherited), + }); + } + let actual = actual_scheme.body(db).ty(db); + diagnostics.extend( + actual_normalizer + .take_errors() + .into_iter() + .map(alias_error_to_diagnostic), + ); + + if !ty_equal(db, expected, actual) { + let inherited_names = type_var_names(db, &inherited); + diagnostics.push(TypeckDiagnostic::InvalidInstanceMethodSignature { + span: LabelSpan::from_span(db, instance_method.sig(db).span(db)), + method: method_name, + reason: format!( + "expected {}, got {}", + display_ty_source(db, expected, &inherited_names), + display_ty_source(db, actual, &inherited_names) + ), + }); + } +} + +fn incomplete_class_method_signature_reason<'db>(sig: &FuncSig<'db>) -> Option { + if sig + .params + .atom() + .iter() + .any(|param| !matches!(param, FuncParam::Typed { .. })) + { + return Some("all parameters must have explicit types".to_owned()); + } + None +} + +fn incomplete_instance_method_signature_reason<'db>(sig: &FuncSig<'db>) -> Option { + if sig + .params + .atom() + .iter() + .any(|param| !matches!(param, FuncParam::Typed { .. })) + { + return Some("all parameters must have explicit types".to_owned()); + } + None +} + +fn scheme_is_ambiguous<'db>(db: &'db dyn Db, scheme: TyScheme<'db>) -> bool { + let body = scheme.body(db); + let preds = body.preds(db); + if preds.is_empty() { + return false; + } + let mut reachable_vars = FxHashSet::default(); + collect_ty_vars(db, body.ty(db), &mut reachable_vars); + let mut changed = true; + while changed { + changed = false; + for pred in preds { + let mut pred_vars = FxHashSet::default(); + collect_pred_vars(db, *pred, &mut pred_vars); + if pred_vars.iter().any(|var| reachable_vars.contains(var)) { + for var in pred_vars { + changed |= reachable_vars.insert(var); + } + } + } + } + let mut all_pred_vars = FxHashSet::default(); + for pred in preds { + collect_pred_vars(db, *pred, &mut all_pred_vars); + } + all_pred_vars + .iter() + .any(|var| !reachable_vars.contains(var)) +} + +fn bind_class_head_vars<'db>( + db: &'db dyn Db, + class_head: Pred<'db>, + instance_head: Pred<'db>, + subst: &mut FxHashMap>, +) -> bool { + match (class_head.kind(db), instance_head.kind(db)) { + ( + PredKind::InClass { + class: class_class, + main: class_main, + args: class_args, + }, + PredKind::InClass { + class: instance_class, + main: instance_main, + args: instance_args, + }, + ) if class_class == instance_class && class_args.len() == instance_args.len() => { + bind_ty_vars(db, *class_main, *instance_main, subst) + && class_args + .iter() + .zip(instance_args) + .all(|(class_arg, instance_arg)| { + bind_ty_vars(db, *class_arg, *instance_arg, subst) + }) + } + _ => false, + } +} + +fn bind_ty_vars<'db>( + db: &'db dyn Db, + pattern: Ty<'db>, + value: Ty<'db>, + subst: &mut FxHashMap>, +) -> bool { + if let TyKind::Comptime(inner) = pattern.kind(db) { + return match value.kind(db) { + TyKind::Comptime(value_inner) => bind_ty_vars(db, *inner, *value_inner, subst), + _ => bind_ty_vars(db, *inner, value, subst), + }; + } + if let TyKind::Comptime(inner) = value.kind(db) { + return bind_ty_vars(db, pattern, *inner, subst); + } + match pattern.kind(db) { + TyKind::BoundVar(var) => match subst.get(&var.index).copied() { + Some(existing) => ty_equal(db, existing, value), + None => { + subst.insert(var.index, value); + true + } + }, + TyKind::Named { ctor, args } => match value.kind(db) { + TyKind::Named { + ctor: value_ctor, + args: value_args, + } if ctor == value_ctor && args.len() == value_args.len() => args + .iter() + .zip(value_args) + .all(|(arg, value_arg)| bind_ty_vars(db, *arg, *value_arg, subst)), + _ => false, + }, + TyKind::Function { params, ret } => match value.kind(db) { + TyKind::Function { + params: value_params, + ret: value_ret, + } if params.len() == value_params.len() => { + params + .iter() + .zip(value_params) + .all(|(param, value_param)| bind_ty_vars(db, *param, *value_param, subst)) + && bind_ty_vars(db, *ret, *value_ret, subst) + } + _ => false, + }, + TyKind::Tuple(elems) => match value.kind(db) { + TyKind::Tuple(value_elems) if elems.len() == value_elems.len() => elems + .iter() + .zip(value_elems) + .all(|(elem, value_elem)| bind_ty_vars(db, *elem, *value_elem, subst)), + _ => false, + }, + TyKind::Comptime(_) => unreachable!("comptime wrappers are stripped before matching"), + TyKind::Error | TyKind::Unknown => true, + } +} + +fn substitute_bound_vars<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + subst: &FxHashMap>, +) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => subst.get(&var.index).copied().unwrap_or(ty), + TyKind::Named { ctor, args } => Ty::named( + db, + *ctor, + args.iter() + .map(|arg| substitute_bound_vars(db, *arg, subst)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| substitute_bound_vars(db, *param, subst)) + .collect(), + substitute_bound_vars(db, *ret, subst), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| substitute_bound_vars(db, *elem, subst)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, substitute_bound_vars(db, *inner, subst)), + TyKind::Error | TyKind::Unknown => ty, + } +} + +fn type_var_bindings_for_instance<'db>( + db: &'db dyn Db, + method: FunctionDef<'db>, + module: Module<'db>, +) -> Vec> { + for item in module.items(db) { + if let Item::InstanceDef(instance) = item + && instance + .methods(db) + .iter() + .any(|candidate| candidate.def_id_value(db) == method.def_id_value(db)) + { + return type_var_bindings(instance.def_id_value(db), instance.type_var_elems(db)); + } + } + Vec::new() +} + +struct ClassLookup<'db> { + class: ClassDef<'db>, + type_vars: Vec>, +} + +fn find_class_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| { + let Item::ClassDef(class) = item else { + return None; + }; + if class.def_id_value(db) != def { + return None; + } + Some(ClassLookup { + class: *class, + type_vars: type_var_bindings(class.def_id_value(db), class.type_var_elems(db)), + }) + }) +} + +fn check_coverage_condition<'db>( + db: &'db dyn Db, + head: Pred<'db>, + span: LabelSpan, + class_name: &str, + type_var_names: &[String], + diagnostics: &mut Vec, +) { + let PredKind::InClass { main, args, .. } = head.kind(db) else { + return; + }; + let mut main_vars = FxHashSet::default(); + collect_ty_vars(db, *main, &mut main_vars); + let mut weak_vars = FxHashSet::default(); + for arg in args { + collect_ty_vars(db, *arg, &mut weak_vars); + } + let undetermined = vars_difference_sorted(&weak_vars, &main_vars); + if undetermined.is_empty() { + return; + } + diagnostics.push(TypeckDiagnostic::CoverageCondition { + span, + class: class_name.to_owned(), + main: display_ty_source(db, *main, type_var_names), + undetermined: display_vars(&undetermined, type_var_names), + }); +} + +fn check_patterson_condition<'db>( + db: &'db dyn Db, + head: Pred<'db>, + span: LabelSpan, + conditions: &[Pred<'db>], + type_var_names: &[String], + diagnostics: &mut Vec, +) { + if conditions + .iter() + .all(|condition| condition.measure(db) < head.measure(db)) + { + return; + } + diagnostics.push(TypeckDiagnostic::PattersonCondition { + span, + head: display_pred_source(db, head, type_var_names), + }); +} + +fn check_bounded_variable_condition<'db>( + db: &'db dyn Db, + head: Pred<'db>, + span: LabelSpan, + conditions: &[Pred<'db>], + diagnostics: &mut Vec, +) { + let mut head_vars = FxHashSet::default(); + collect_pred_vars(db, head, &mut head_vars); + for condition in conditions { + let mut condition_vars = FxHashSet::default(); + collect_pred_vars(db, *condition, &mut condition_vars); + if condition_vars.iter().any(|var| !head_vars.contains(var)) { + diagnostics.push(TypeckDiagnostic::BoundedVariableCondition { span }); + return; + } + } +} + +fn head_ref_class_name<'db>(db: &'db dyn Db, pred: hir::ast::ty::PredRef<'db>) -> String { + (*pred.kind(db).class.atom()).text(db).to_owned() +} + +fn type_var_names<'db>(db: &'db dyn Db, vars: &[hir_nameres::TypeVarBinding<'db>]) -> Vec { + vars.iter() + .map(|var| (*var.name.atom()).text(db).to_owned()) + .collect() +} + +fn vars_difference_sorted(left: &FxHashSet, right: &FxHashSet) -> Vec { + let mut vars = left + .iter() + .copied() + .filter(|var| !right.contains(var)) + .collect::>(); + vars.sort_unstable(); + vars +} diff --git a/crates/hir-ty/src/support.rs b/crates/hir-ty/src/support.rs new file mode 100644 index 00000000..79a399f3 --- /dev/null +++ b/crates/hir-ty/src/support.rs @@ -0,0 +1,36 @@ +use hir::anchor::DefId; +use nameres::{LibraryId, ModuleId, module_id_from_key, module_key_for_path, reachable_modules}; + +use crate::Db; + +pub(crate) fn module_for_def_via_graph<'db>( + db: &'db dyn Db, + entry: ModuleId<'db>, + def: DefId<'db>, +) -> Option> { + let file = def.file(db); + reachable_modules(db, entry) + .into_iter() + .find(|module| db.module_file(*module) == Some(file)) +} + +pub(crate) fn module_for_def_via_tree<'db>( + db: &'db dyn Db, + def: DefId<'db>, +) -> Option> { + let path = hir::url_to_file_path(def.file(db).url(db))?; + let tree = db.module_tree(); + let candidates = std::iter::once((LibraryId::Main, tree.main_root(db).clone())) + .chain(std::iter::once((LibraryId::Std, tree.std_root(db).clone()))) + .chain( + tree.external_roots(db) + .iter() + .map(|(name, root)| (LibraryId::External(name.clone()), root.clone())), + ); + for (library, root) in candidates { + if let Some(key) = module_key_for_path(library, &root, &path) { + return Some(module_id_from_key(db, &key)); + } + } + None +} diff --git a/crates/hir-ty/src/value_type.rs b/crates/hir-ty/src/value_type.rs new file mode 100644 index 00000000..7fe1ca78 --- /dev/null +++ b/crates/hir-ty/src/value_type.rs @@ -0,0 +1,322 @@ +//! User-defined value-type declaration lookup and representation lowering. +//! +//! Unlike transparent aliases, a value type remains nominal in semantic types. +//! This module is the single boundary where consumers intentionally recover +//! its underlying runtime representation. + +use hir::{ + Db as HirDb, + anchor::DefId, + ast::item::{ContractItem, Item, Module, TypeAlias, TypeAliasKind}, + diag::{LabelSpan, Offset}, + nameres::{self as hir_nameres, type_var_bindings}, + span::{AnchorId, Span, Spanned}, +}; +use nameres::LibraryId; +use parser::parse_file_to_hir; + +use crate::{ + AliasNormalizer, BinderEnv, BuiltinTyCtor, Db, Ty, TyCtor, TyKind, TypeLowering, UserTyCtorKind, +}; + +/// A malformed or unavailable user-defined value-type declaration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValueTypeError { + /// Declaration name, when it can be recovered. + pub name: String, + /// Declaration or definition span. + pub span: LabelSpan, + /// Human-readable rejection reason. + pub reason: String, +} + +struct ValueTypeInfo<'db> { + declaration: TypeAlias<'db>, + inherited_type_vars: Vec>, +} + +/// Resolves a value type through the supplied module context. +/// +/// Inference uses this form so standalone databases and compiler-owned HIR +/// overlays do not need a registered inter-module tree. If `def` is external +/// to `module`, lookup falls back to its registered source module. +pub fn value_type_underlying_in_context<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + def: DefId<'db>, +) -> Result, ValueTypeError> { + if let Some(info) = find_value_type_info(db, module, def) { + return lower_value_type_info(db, module, item_resolutions, info); + } + value_type_underlying(db, def) +} + +/// Resolves a value type from its registered source module. +/// +/// This path deliberately avoids frontend preparation/type-inference queries: +/// downstream layout and ABI code may call it while those queries are already +/// active. The module import surface provides import-aware type resolution +/// directly from parsed HIR. +pub fn value_type_underlying<'db>( + db: &'db dyn Db, + def: DefId<'db>, +) -> Result, ValueTypeError> { + if let Some(module_id) = nameres::module_id_for_source_file(db, def.file(db)) { + let env = nameres::module_import_surface(db, module_id); + if let Some(scope) = env.item_scope.as_ref() { + let item_resolutions = + hir_nameres::resolve_item_type_facts_with_imports(db, scope.module, scope, &env); + if let Some(info) = find_value_type_info(db, scope.module, def) { + return lower_value_type_info(db, scope.module, &item_resolutions, info); + } + } + } + + // Standalone analysis databases do not necessarily register a module + // tree. Local declarations with builtin-only underlyings remain fully + // resolvable directly from parsed HIR; imported underlyings still fail + // safely instead of being guessed. + let module = parse_file_to_hir(db, def.file(db)).module(db); + let item_resolutions = hir_nameres::resolve_item_type_facts(db, module); + let Some(info) = find_value_type_info(db, module, def) else { + return Err(missing_value_type(db, def)); + }; + lower_value_type_info(db, module, &item_resolutions, info) +} + +fn lower_value_type_info<'db>( + db: &'db dyn Db, + module: Module<'db>, + item_resolutions: &hir_nameres::ItemResolutionFacts<'db>, + info: ValueTypeInfo<'db>, +) -> Result, ValueTypeError> { + let declaration = info.declaration; + let name = declaration_name(db, declaration); + let span = LabelSpan::from_span(db, declaration.name_elem(db).span(db)); + if !declaration.ty_param_elems(db).is_empty() { + return Err(ValueTypeError { + name, + span, + reason: "user-defined value types cannot declare type parameters".to_owned(), + }); + } + if !info.inherited_type_vars.is_empty() { + return Err(ValueTypeError { + name, + span, + reason: + "a user-defined value type cannot be declared inside a generic contract context" + .to_owned(), + }); + } + + let lowerer = TypeLowering::from_item_resolutions( + db, + item_resolutions, + BinderEnv::from_type_vars(&info.inherited_type_vars), + ); + let lowered = lowerer.lower_type_alias(declaration).ty; + if !lowerer.take_diagnostics().is_empty() { + return Err(ValueTypeError { + name, + span, + reason: "underlying type could not be lowered".to_owned(), + }); + } + let mut normalizer = AliasNormalizer::new(db, module, item_resolutions); + let underlying = normalizer.normalize_ty(lowered); + if !normalizer.take_errors().is_empty() { + return Err(ValueTypeError { + name, + span, + reason: "underlying type contains an invalid transparent alias".to_owned(), + }); + } + validate_underlying(db, underlying).map_err(|reason| ValueTypeError { name, span, reason })?; + Ok(underlying) +} + +fn validate_underlying(db: &dyn Db, ty: Ty<'_>) -> Result<(), String> { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word | BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Ok(()), + TyKind::Named { + ctor: + TyCtor::User(user @ crate::UserTyCtor { + kind: UserTyCtorKind::Adt, + .. + }), + args, + } if args.is_empty() && is_canonical_std_elementary_value(db, user.def) => { + Ok(()) + } + TyKind::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + kind: UserTyCtorKind::ValueType, + .. + }), + .. + } => Err( + "a user-defined value type cannot use another user-defined value type as its underlying type" + .to_owned(), + ), + TyKind::Function { .. } => { + Err("function types cannot underlie a user-defined value type".to_owned()) + } + TyKind::Tuple(_) => Err("tuple types cannot underlie a user-defined value type".to_owned()), + TyKind::Comptime(_) | TyKind::BoundVar(_) => Err( + "the underlying type must be a concrete runtime elementary value type".to_owned(), + ), + TyKind::Error | TyKind::Unknown => { + Err("the underlying type could not be resolved".to_owned()) + } + TyKind::Named { .. } => Err( + "the underlying type must be `word`, `bool`, or a Solidity elementary value type" + .to_owned(), + ), + } +} + +/// Returns whether a valid value-type underlying has the one-word storage +/// representation currently supported by the backend. +/// +/// `bool` is intentionally excluded: Hull represents it as a tagged sum, so +/// treating it as a raw storage word would cross the backend's I1/I256 +/// boundary without the required encoding and validation. +pub fn value_type_underlying_has_word_storage_representation(db: &dyn Db, ty: Ty<'_>) -> bool { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } => args.is_empty(), + TyKind::Named { + ctor: + TyCtor::User(crate::UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } => args.is_empty() && is_canonical_std_elementary_value(db, *def), + _ => false, + } +} + +fn is_canonical_std_elementary_value(db: &dyn Db, def: DefId<'_>) -> bool { + let Some(name) = def.name(db) else { + return false; + }; + let Some(module) = nameres::module_id_for_source_file(db, def.file(db)) else { + return false; + }; + if module.library(db) != &LibraryId::Std || module.logical_path(db).as_slice() != ["std"] { + return false; + } + is_solidity_elementary_value_name(&name) +} + +fn is_solidity_elementary_value_name(name: &str) -> bool { + if matches!(name, "address" | "byte") { + return true; + } + if let Some(bits) = name + .strip_prefix("uint") + .or_else(|| name.strip_prefix("int")) + { + return valid_bit_width(bits, 8, 256, 8); + } + if let Some(bytes) = name.strip_prefix("bytes") { + return valid_bit_width(bytes, 1, 32, 1); + } + false +} + +fn valid_bit_width(text: &str, min: u16, max: u16, step: u16) -> bool { + text.parse::() + .is_ok_and(|value| value >= min && value <= max && value % step == 0) +} + +fn find_value_type_info<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_value_type_in_item(db, *item, def)) +} + +fn find_value_type_in_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + def: DefId<'db>, +) -> Option> { + match item { + Item::TypeAlias(declaration) + if declaration.def_id_value(db) == def + && declaration.kind(db) == TypeAliasKind::ValueType => + { + Some(ValueTypeInfo { + declaration, + inherited_type_vars: Vec::new(), + }) + } + Item::ContractDef(contract) => { + let inherited_type_vars = + type_var_bindings(contract.def_id_value(db), contract.ty_param_elems(db)); + contract.items(db).iter().find_map(|item| match *item { + ContractItem::TypeAlias(declaration) + if declaration.def_id_value(db) == def + && declaration.kind(db) == TypeAliasKind::ValueType => + { + Some(ValueTypeInfo { + declaration, + inherited_type_vars: inherited_type_vars.clone(), + }) + } + _ => None, + }) + } + _ => None, + } +} + +fn declaration_name(db: &dyn HirDb, declaration: TypeAlias<'_>) -> String { + declaration + .def_id_value(db) + .name(db) + .unwrap_or_else(|| "".to_owned()) +} + +fn missing_value_type(db: &dyn HirDb, def: DefId<'_>) -> ValueTypeError { + let span = Span::new( + AnchorId::root(db, def.file(db)), + Offset::new(0), + Offset::new(0), + ); + ValueTypeError { + name: def + .name(db) + .unwrap_or_else(|| "".to_owned()), + span: LabelSpan::from_span(db, span), + reason: "value-type declaration is unavailable".to_owned(), + } +} + +#[cfg(test)] +mod tests { + use super::is_solidity_elementary_value_name; + + #[test] + fn static_byte_names_are_elementary_but_dynamic_bytes_is_not() { + assert!(is_solidity_elementary_value_name("byte")); + assert!(is_solidity_elementary_value_name("bytes1")); + assert!(is_solidity_elementary_value_name("bytes32")); + assert!(!is_solidity_elementary_value_name("bytes")); + assert!(!is_solidity_elementary_value_name("bytes33")); + } +} diff --git a/crates/hir-ty/tests/contract_semantics.rs b/crates/hir-ty/tests/contract_semantics.rs new file mode 100644 index 00000000..ea36680a --- /dev/null +++ b/crates/hir-ty/tests/contract_semantics.rs @@ -0,0 +1,1972 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, +}; + +use hir::{ + anchor::DefLocationTable, + ast::{ + function::FunctionMutability, + item::{AdtDef, ContractDef, FunctionDef, Item, Module}, + }, + diag::{Diagnostic, DiagnosticCode}, + input::SourceFile, +}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, +}; +use parser::parse_file_to_hir; +use rustc_hash::FxHashMap; +use salsa::Setter; +use solcore_hir_ty::{ + BinderEnv, BuiltinTyCtor, CallSiteCallee, DispatchConstructor, DispatchFallback, + FieldInitPreTypeckTransform, FrontendTransform, IndirectArgShape, PreTypeckTransform, + ProductShape, SourceOriginKind, Ty, TyCtor, TyKind, TypeLowering, contract_abi_json, + contract_dispatch_surface, derived_generic_instance_plan, derived_generic_plan, + frontend_desugar_plan, function_scheme, infer::module_typeck_diagnostics, normalize_ty_aliases, + pre_typeck_desugar_plan, prepare_module, +}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, + existing_files: BTreeSet, +} + +impl TestDb { + fn sync_inputs(&mut self) { + let existing_files = self.existing_files.clone(); + if let Some(snapshot) = self.module_fs_snapshot { + if snapshot.existing_files(self) != &existing_files { + snapshot.set_existing_files(self).to(existing_files); + } + } else { + self.module_fs_snapshot = + Some(ModuleFsSnapshot::new(self, existing_files, BTreeMap::new())); + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + if snapshot.files(self) != &files { + snapshot.set_files(self).to(files); + } + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + ModuleTree::new( + self, + PathBuf::from("/main"), + PathBuf::from("/std"), + BTreeMap::new(), + ) + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .unwrap_or_else(|| ModuleFsSnapshot::new(self, BTreeSet::new(), BTreeMap::new())) + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .unwrap_or_else(|| ModuleFileSnapshot::new(self, BTreeMap::new())) + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl solcore_hir_ty::Db for TestDb {} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn source_file_at(db: &TestDb, path: &str, src: &str) -> SourceFile { + let url = url::Url::from_file_path(path).expect("absolute source path"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn parse_module<'db>(db: &'db TestDb, src: &str) -> Module<'db> { + parse_file_to_hir(db, source_file(db, "contract_semantics", src)).module(db) +} + +#[test] +fn contract_local_alias_normalization_separates_inherited_and_explicit_type_vars() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract C { + alias Fixed = word[3]; + alias Element = t[3]; + alias Generic = a[3]; + + fixed: Fixed; + element: Element; + generic: Generic; +} +"#, + ); + let contract = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + let resolutions = hir::nameres::resolve_item_types(&db, module); + let type_vars = + hir::nameres::type_var_bindings(contract.def_id_value(&db), contract.ty_param_elems(&db)); + let lowerer = TypeLowering::from_item_resolutions( + &db, + &resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + + for field in contract.fields(&db) { + let normalized = + normalize_ty_aliases(&db, module, &resolutions, lowerer.lower_field(field).ty); + assert!( + normalized.errors.is_empty(), + "{}: {:?}", + field.name().atom().text(&db), + normalized.errors + ); + let TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(length)), + args, + } = normalized.value.kind(&db) + else { + panic!( + "{} did not normalize to a fixed array: {}", + field.name().atom().text(&db), + normalized.value.display(&db) + ); + }; + assert_eq!(*length, 3); + assert_eq!(args.len(), 1); + match field.name().atom().text(&db) { + "fixed" | "generic" => assert!(matches!( + args[0].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args + } if args.is_empty() + )), + "element" => assert!(matches!( + args[0].kind(&db), + TyKind::BoundVar(var) if var.index == 0 + )), + other => panic!("unexpected field {other}"), + } + } +} + +fn db_with_main(src: &str) -> (TestDb, ModuleKey) { + let mut db = TestDb::default(); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + let path = PathBuf::from("/main/main.solc"); + let file = source_file_at(&db, "/main/main.solc", src); + db.existing_files.insert(path); + db.module_files.insert(key.clone(), file); + db.sync_inputs(); + (db, key) +} + +fn insert_module_source(db: &mut TestDb, key: ModuleKey, path: &str, src: &str) { + let file = source_file_at(db, path, src); + db.existing_files.insert(PathBuf::from(path)); + db.module_files.insert(key, file); + db.sync_inputs(); +} + +fn insert_real_std_modules(db: &mut TestDb) { + for (logical, path, source) in [ + ( + "std", + "/std/std.solc", + include_str!("../../../std/std.solc"), + ), + ( + "dispatch", + "/std/dispatch.solc", + include_str!("../../../std/dispatch.solc"), + ), + ( + "opcodes", + "/std/opcodes.solc", + include_str!("../../../std/opcodes.solc"), + ), + ( + "Generic", + "/std/Generic.solc", + include_str!("../../../std/Generic.solc"), + ), + ( + "ABIGeneric", + "/std/ABIGeneric.solc", + include_str!("../../../std/ABIGeneric.solc"), + ), + ] { + insert_module_source( + db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec![logical.to_owned()], + }, + path, + source, + ); + } +} + +fn contract_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> ContractDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) + if contract.def_id_value(db).name(db).as_deref() == Some(name) => + { + Some(*contract) + } + _ => None, + }) + .expect("contract") +} + +fn adt_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> AdtDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::AdtDef(adt) if adt.def_id_value(db).name(db).as_deref() == Some(name) => { + Some(*adt) + } + _ => None, + }) + .expect("adt") +} + +fn function_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FunctionDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::FunctionDef(function) + if function.def_id_value(db).name(db).as_deref() == Some(name) => + { + Some(*function) + } + _ => None, + }) + .expect("function") +} + +fn pair_args<'db>(db: &'db TestDb, ty: Ty<'db>) -> Option<&'db Vec>> { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => Some(args), + _ => None, + } +} + +fn product_is_pair(shape: &ProductShape) -> bool { + matches!(shape, ProductShape::Pair { tail, .. } if matches!(tail.as_ref(), ProductShape::Single(_))) +} + +fn product_is_triple(shape: &ProductShape) -> bool { + matches!( + shape, + ProductShape::Pair { tail, .. } + if matches!( + tail.as_ref(), + ProductShape::Pair { tail, .. } + if matches!(tail.as_ref(), ProductShape::Single(_)) + ) + ) +} + +fn diagnostics(src: &str) -> Vec { + let (db, key) = db_with_main(src); + diagnostics_for_module(&db, &key) +} + +fn diagnostics_for_module(db: &TestDb, key: &ModuleKey) -> Vec { + let module = module_id_from_key(db, key); + module_typeck_diagnostics(db, module) + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect() +} + +#[test] +fn generated_dispatch_is_synthesized_before_import_resolution() { + let db = TestDb::default(); + let source = parse_module( + &db, + r#" +contract Answer { + function add(x: word) public returns (word) { return x; } +} +"#, + ); + let contract = contract_named(&db, source, "Answer"); + let prepared = prepare_module(&db, source); + assert!( + prepared + .contract_dispatch_main(&db, contract.def_id_value(&db)) + .is_some(), + "dispatch synthesis is syntactic; type checking still requires explicit imports" + ); + assert_eq!(prepared.source(&db), source); + + let manual_db = TestDb::default(); + let manual_source = parse_module( + &manual_db, + r#" +contract Answer { + function main() returns () { return (); } +} +"#, + ); + let manual_contract = contract_named(&manual_db, manual_source, "Answer"); + assert!( + prepare_module(&manual_db, manual_source) + .contract_dispatch_main(&manual_db, manual_contract.def_id_value(&manual_db)) + .is_none() + ); + + let parameterized_main = diagnostics( + r#" +contract Answer { + function main(x: word) public returns (word) { return x; } +} +"#, + ); + assert!( + parameterized_main.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some(DiagnosticCode::TYPECK_CONTRACT_RUNTIME_MAIN_ARITY) + }), + "{parameterized_main:?}" + ); +} + +#[test] +fn prepared_dispatch_uses_its_synthetic_sigstring_instance_during_typeck() { + let (mut db, key) = db_with_main( + r#" +import std; +import std.dispatch; + +contract Answer { + function ping(x: word) external view returns (word) { return x; } +} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["std".to_owned()], + }, + "/std/std.solc", + r#" +export { Proxy(*), string }; +enum Proxy { Proxy } +enum string {} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["dispatch".to_owned()], + }, + "/std/dispatch.solc", + r#" +import std; + +export { + Contract(*), + Fallback(*), + Method(*), + NonPayable, + Payable, + RunContract, + SigString, + fallback_default_implementation +}; + +enum Contract { Contract(methods, fb) } +enum Method { Method(Proxy, Proxy, Proxy, Proxy, fn) } +enum Fallback { Fallback(Proxy, Proxy, Proxy, fn) } +enum Payable {} +enum NonPayable {} + +trait SigString { + function sigStr(value: Proxy) returns (string) ; +} + +trait RunContract { + function exec(value: c) returns () ; +} + +impl RunContract, fb>> where name: SigString { + function exec(value: Contract, fb>) returns () { + return (); + } +} + +function fallback_default_implementation() returns () { return (); } +"#, + ); + + let module_id = module_id_from_key(&db, &key); + let file = db.module_files.get(&key).copied().expect("main module"); + let source = parse_file_to_hir(&db, file).module(&db); + let effective = prepare_module(&db, source).module(&db); + assert_ne!( + effective, source, + "dispatch preparation should create an overlay" + ); + + let env = nameres::module_env_for_hir_module(&db, module_id, effective); + let scope = env.item_scope.clone().expect("prepared item scope"); + let resolution = hir::nameres::resolve_module_with_imports(&db, effective, scope, &env); + assert!( + resolution.diagnostics.is_empty(), + "prepared HIR must resolve against its overlay scope: {:?}", + resolution.diagnostics + ); + + let diagnostics = diagnostics_for_module(&db, &key); + assert!( + diagnostics.is_empty(), + "the local generated SigString impl must be in the prepared trait environment: {diagnostics:?}" + ); +} + +#[test] +fn dispatch_surface_tracks_public_private_constructor_and_fallback() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Token { + constructor(amount: word) payable {} + + function hidden(x: word) returns (word) { return x; } + + function pay(to: word) public payable returns (word, bool) { + return (to, true); + } + + fallback() payable returns () {} +} +"#, + ); + let contract = contract_named(&db, module, "Token"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!(surface.name, "Token"); + let DispatchConstructor::Explicit { + payable, inputs, .. + } = &surface.constructor + else { + panic!("expected explicit constructor: {:?}", surface.constructor); + }; + assert!(*payable); + assert_eq!(inputs[0].name, "amount"); + assert_eq!(inputs[0].ty.to_string(), "uint256"); + let DispatchFallback::Explicit { payable, .. } = &surface.fallback else { + panic!("expected explicit fallback: {:?}", surface.fallback); + }; + assert!(*payable); + assert_eq!(surface.methods.len(), 1); + assert_eq!(surface.methods[0].name, "pay"); + assert_eq!( + surface.methods[0].mutability, + Some(FunctionMutability::Payable) + ); + assert_eq!(surface.methods[0].signature, "pay(uint256)"); + assert_eq!(surface.methods[0].selector.to_hex(), "0xc290d691"); + assert_eq!(surface.methods[0].outputs[0].ty.to_string(), "uint256"); + assert_eq!(surface.methods[0].outputs[1].ty.to_string(), "bool"); +} + +#[test] +fn abi_json_matches_reference_public_function_shape() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Sample { + function get() public returns (word) { return 1; } + function secret() returns (word) { return 0; } +} +"#, + ); + let contract = contract_named(&db, module, "Sample"); + + let abi = contract_abi_json(&db, module, contract).expect("ABI JSON"); + let expected = concat!( + "[\n", + " {\n", + " \"inputs\": [],\n", + " \"name\": \"get\",\n", + " \"outputs\": [\n", + " {\n", + " \"internalType\": \"uint256\",\n", + " \"name\": \"\",\n", + " \"type\": \"uint256\"\n", + " }\n", + " ],\n", + " \"stateMutability\": \"nonpayable\",\n", + " \"type\": \"function\"\n", + " }\n", + "]\n" + ); + assert_eq!(abi, expected); +} + +#[test] +fn external_abi_preserves_visibility_and_all_state_mutability_modes() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Modes { + function pure_fn() public pure returns (word) { return 1; } + function view_fn() external view returns (word) { return 2; } + function default_fn() public returns (word) { return 3; } + function payable_fn() external payable returns (word) { return 4; } + function internal_fn() internal pure returns (word) { return 5; } + function private_fn() private view returns (word) { return 6; } +} +"#, + ); + let contract = contract_named(&db, module, "Modes"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!(surface.methods.len(), 4, "{:#?}", surface.methods); + assert_eq!( + surface + .methods + .iter() + .map(|method| (method.name.as_str(), method.mutability)) + .collect::>(), + vec![ + ("pure_fn", Some(FunctionMutability::Pure)), + ("view_fn", Some(FunctionMutability::View)), + ("default_fn", None), + ("payable_fn", Some(FunctionMutability::Payable)), + ] + ); + + let abi = contract_abi_json(&db, module, contract).expect("ABI JSON"); + for (name, expected) in [ + ("pure_fn", "pure"), + ("view_fn", "view"), + ("default_fn", "nonpayable"), + ("payable_fn", "payable"), + ] { + let name_offset = abi + .find(&format!("\"name\": \"{name}\"")) + .unwrap_or_else(|| panic!("missing `{name}` entry: {abi}")); + let entry_end = abi[name_offset..] + .find("\n }") + .map(|offset| name_offset + offset) + .unwrap_or(abi.len()); + assert!( + abi[name_offset..entry_end].contains(&format!("\"stateMutability\": \"{expected}\"")), + "wrong state mutability for `{name}`: {abi}" + ); + } + assert!(!abi.contains("\"name\": \"internal_fn\""), "{abi}"); + assert!(!abi.contains("\"name\": \"private_fn\""), "{abi}"); +} + +#[test] +fn public_value_types_are_rejected_from_the_external_abi() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +type Wad is word; + +contract Vault { + function set(amount: Wad) public returns (Wad) { return amount; } +} +"#, + ); + let contract = contract_named(&db, module, "Vault"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!(surface.methods[0].signature, "set()"); + assert_eq!(surface.methods[0].inputs[0].ty.to_string(), ""); + assert_eq!( + surface.methods[0].outputs[0].ty.to_string(), + "" + ); + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic.message.contains("user-defined value types") + }), + "{:?}", + surface.diagnostics + ); + assert!( + contract_abi_json(&db, module, contract) + .expect_err("UDVT must fail closed in ABI JSON") + .contains("unsupported type") + ); +} + +#[test] +fn fixed_arrays_fail_closed_in_nested_and_wrapped_external_abi_positions() { + let (db, key) = db_with_main( + r#" +enum memory { memory(t) } + +contract Arrays { + constructor(seed: word[4] memory) {} + + function roundtrip(value: (word, bool[2])) public returns (word[3] memory) { + revert; + } +} +"#, + ); + let file = db.module_files[&key]; + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "Arrays"); + let surface = contract_dispatch_surface(&db, module, contract); + + let DispatchConstructor::Explicit { inputs, .. } = &surface.constructor else { + panic!("expected explicit constructor: {:?}", surface.constructor); + }; + assert_eq!(inputs[0].ty.to_string(), ""); + assert_eq!(surface.methods[0].signature, "roundtrip()"); + assert_eq!(surface.methods[0].inputs[0].ty.to_string(), ""); + assert_eq!( + surface.methods[0].outputs[0].ty.to_string(), + "" + ); + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic.message.contains("fixed-length arrays") + }), + "{:?}", + surface.diagnostics + ); + assert!( + contract_abi_json(&db, module, contract) + .expect_err("fixed arrays must fail closed in ABI JSON") + .contains("unsupported type") + ); +} + +#[test] +fn canonical_std_uint256_value_type_typechecks_internally_but_is_not_public_abi_safe() { + let (mut db, key) = db_with_main( + r#" +import std; + +type Wad is uint256; + +function roundtrip(amount: Wad) returns (Wad) { + return (amount as uint256) as Wad; +} +"#, + ); + insert_real_std_modules(&mut db); + let module_id = module_id_from_key(&db, &key); + let diagnostics = module_typeck_diagnostics(&db, module_id); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); + + let abi_key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["abi".to_owned()], + }; + insert_module_source( + &mut db, + abi_key.clone(), + "/main/abi.solc", + r#" +import std; + +type Wad is uint256; + +contract Vault { + function echo(amount: Wad) public returns (Wad) { + return amount; + } +} +"#, + ); + let file = db.module_files[&abi_key]; + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "Vault"); + let surface = contract_dispatch_surface(&db, module, contract); + assert_eq!(surface.methods[0].signature, "echo()"); + assert_eq!(surface.methods[0].inputs[0].ty.to_string(), ""); + assert_eq!( + surface.methods[0].outputs[0].ty.to_string(), + "" + ); + assert!( + surface + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0231")), + "{:?}", + surface.diagnostics + ); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn imported_value_types_are_rejected_recursively_from_methods_and_constructors() { + let (mut db, key) = db_with_main( + r#" +import { Wad } from types; + +contract Vault { + constructor(seed: (word, (Wad, bool))) {} + + function nested(value: (word, (Wad, bool))) public returns ((bool, Wad)) { + revert; + } +} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["types".to_owned()], + }, + "/main/types.solc", + "export { Wad }; type Wad is word;", + ); + + let file = db.module_files[&key]; + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "Vault"); + let surface = contract_dispatch_surface(&db, module, contract); + + let DispatchConstructor::Explicit { inputs, .. } = &surface.constructor else { + panic!("expected explicit constructor: {:?}", surface.constructor); + }; + assert_eq!(inputs[0].ty.to_string(), ""); + assert_eq!(surface.methods[0].signature, "nested()"); + assert_eq!(surface.methods[0].inputs[0].ty.to_string(), ""); + assert_eq!(surface.methods[0].outputs[0].ty.to_string(), "bool"); + assert_eq!( + surface.methods[0].outputs[1].ty.to_string(), + "" + ); + assert!( + surface + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.as_deref() == Some("SC0231")) + .count() + >= 4, + "{:?}", + surface.diagnostics + ); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn internal_value_types_do_not_poison_an_otherwise_supported_public_abi() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +type Wad is word; + +contract Vault { + function hidden(value: Wad) returns (Wad) { + return value; + } + + function echo(value: word) public returns (word) { + return value; + } +} +"#, + ); + let contract = contract_named(&db, module, "Vault"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!(surface.methods.len(), 1); + assert_eq!(surface.methods[0].name, "echo"); + assert_eq!(surface.methods[0].signature, "echo(uint256)"); + assert!( + surface + .diagnostics + .iter() + .all(|diagnostic| diagnostic.code.as_deref() != Some("SC0231")), + "{:?}", + surface.diagnostics + ); + assert!(contract_abi_json(&db, module, contract).is_ok()); +} + +#[test] +fn public_value_type_reports_sc0231_alongside_solver_failure() { + let (mut db, key) = db_with_main( + r#" +import std; +import std.dispatch; + +type Wad is word; + +contract Vault { + function echo(value: Wad) public returns (Wad) { + return value; + } +} +"#, + ); + insert_real_std_modules(&mut db); + let diagnostics = diagnostics_for_module(&db, &key); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0207")), + "{diagnostics:#?}" + ); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0231")), + "{diagnostics:#?}" + ); +} + +#[test] +fn named_return_typechecks_and_preserves_abi_output_names() { + let typeck_diagnostics = diagnostics( + r#" +function named(x: word) returns (result: word) { + result = x; + return result; +} +"#, + ); + assert!( + typeck_diagnostics.is_empty(), + "named result assignment and reference should typecheck: {typeck_diagnostics:?}" + ); + + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Named { + function pair(x: word) public returns (first: word, bool) { + first = x; + return (first, true); + } +} +"#, + ); + let contract = contract_named(&db, module, "Named"); + let surface = contract_dispatch_surface(&db, module, contract); + assert_eq!(surface.methods.len(), 1); + assert_eq!(surface.methods[0].outputs.len(), 2); + assert_eq!(surface.methods[0].outputs[0].name, "first"); + assert_eq!(surface.methods[0].outputs[1].name, ""); + + let abi = contract_abi_json(&db, module, contract).expect("ABI JSON"); + assert!(abi.contains("\"name\": \"first\""), "{abi}"); +} + +#[test] +fn abi_json_matches_reference_constructor_payable_and_tuple_outputs() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Token { + constructor(amount: word) {} + + function pay(to: word) public payable returns (word, bool) { + return (to, true); + } +} +"#, + ); + let contract = contract_named(&db, module, "Token"); + + let abi = contract_abi_json(&db, module, contract).expect("ABI JSON"); + assert!(abi.contains("\"type\": \"constructor\"")); + assert!(abi.contains("\"name\": \"amount\"")); + assert!(abi.contains("\"stateMutability\": \"payable\"")); + assert!(abi.contains("\"type\": \"bool\"")); +} + +#[test] +fn abi_json_preserves_source_declaration_order() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Order { + function a() public returns (word) { return 1; } + constructor(seed: word) {} + fallback() payable returns () {} + function b(x: word) public returns (word) { return x; } +} +"#, + ); + let contract = contract_named(&db, module, "Order"); + + let abi = contract_abi_json(&db, module, contract).expect("ABI JSON"); + let a = abi.find("\"name\": \"a\"").expect("a entry"); + let constructor = abi + .find("\"type\": \"constructor\"") + .expect("constructor entry"); + let fallback = abi.find("\"type\": \"fallback\"").expect("fallback entry"); + let b = abi.find("\"name\": \"b\"").expect("b entry"); + assert!( + a < constructor && constructor < fallback && fallback < b, + "{abi}" + ); +} + +#[test] +fn constructor_and_fallback_abi_lowering_normalizes_aliases() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +alias U = word; +alias UnitAlias = (); + +contract AliasDispatch { + constructor(seed: U) {} + fallback() returns (UnitAlias) {} +} +"#, + ); + let contract = contract_named(&db, module, "AliasDispatch"); + let surface = contract_dispatch_surface(&db, module, contract); + + let DispatchConstructor::Explicit { inputs, .. } = &surface.constructor else { + panic!("expected explicit constructor: {:?}", surface.constructor); + }; + assert_eq!(inputs[0].ty.to_string(), "uint256"); + let DispatchFallback::Explicit { outputs, .. } = &surface.fallback else { + panic!("expected explicit fallback: {:?}", surface.fallback); + }; + assert!(outputs.is_empty(), "{outputs:?}"); + assert!( + surface + .diagnostics + .iter() + .all(|diagnostic| diagnostic.code.as_deref() != Some("SC0231")), + "{:?}", + surface.diagnostics + ); +} + +#[test] +fn dispatch_signature_spelling_matches_reference_sigstring_shape() { + let (mut db, key) = db_with_main( + r#" +import std; + +alias U = word; + +contract Signatures { + function spell(a: word, b: (word, bool), c: string memory, d: bytes memory, e: bytes32, f: address, g: U) public returns (word) { + return a; + } +} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["std".to_owned()], + }, + "/std/std.solc", + r#" +export { string, address(*), bytes, bytes32(*), memory(*) }; +enum string {} +enum address { address(word) } +enum bytes {} +enum bytes32 { bytes32(word) } +enum memory { memory(word) } +"#, + ); + let file = db.module_files[&key]; + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "Signatures"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!( + surface.methods[0].signature, + "spell(uint256,(uint256,bool),string,bytes,bytes32,address,uint256)" + ); +} + +#[test] +fn single_constructor_adt_is_rejected_from_the_canonical_abi() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Point { Point(a, bool) } + +contract Shapes { + function roundtrip(p: Point) public returns (Point) { return p; } +} +"#, + ); + let contract = contract_named(&db, module, "Shapes"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("user-defined ADTs are not supported by the canonical external ABI") + }), + "{:?}", + surface.diagnostics + ); + let method = &surface.methods[0]; + assert_eq!(method.signature, "roundtrip()"); + assert_eq!(method.inputs[0].ty.to_string(), ""); + assert_eq!(method.outputs[0].ty.to_string(), ""); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn tuple_typed_constructor_field_does_not_make_a_user_adt_abi_safe() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Wrap { Wrap((word, bool)) } + +contract Shapes { + function roundtrip(value: Wrap) public returns (Wrap) { return value; } +} +"#, + ); + let contract = contract_named(&db, module, "Shapes"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("user-defined ADTs are not supported") + }), + "{:?}", + surface.diagnostics + ); + assert_eq!(surface.methods[0].signature, "roundtrip()"); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn user_defined_location_name_does_not_make_an_adt_abi_safe() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum memory { memory(word) } +enum Wrap { Wrap((word, bool) memory) } + +contract Shapes { + function roundtrip(value: Wrap) public returns (Wrap) { return value; } +} +"#, + ); + let contract = contract_named(&db, module, "Shapes"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("user-defined ADTs are not supported") + }), + "{:?}", + surface.diagnostics + ); + assert_eq!(surface.methods[0].signature, "roundtrip()"); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn multi_constructor_adt_is_rejected_from_the_canonical_abi() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Choice { Left(word), Right(bool) } + +contract Shapes { + function choose(x: Choice) public returns (word) { return 0; } +} +"#, + ); + let contract = contract_named(&db, module, "Shapes"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("user-defined ADTs are not supported") + }), + "{:?}", + surface.diagnostics + ); + assert_eq!(surface.methods[0].signature, "choose()"); + assert!(contract_abi_json(&db, module, contract).is_err()); +} + +#[test] +fn visible_orphan_generic_instance_is_rejected_from_constructor_abi() { + let (mut db, key) = db_with_main( + r#" +import std; +import std.dispatch; +import model; + +pragma solcore noGenericInstanceFor Payload; + +impl Generic {} + +contract C { + constructor(payload: Payload) {} + function roundtrip(payload: Payload) public returns (Payload) { return payload; } +} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["std".to_owned()], + }, + "/std/std.solc", + r#" +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; +export { Generic }; +trait Generic { + function from(x: a) returns (rep) ; + function to(x: rep) returns (a) ; +} +"#, + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["dispatch".to_owned()], + }, + "/std/dispatch.solc", + "", + ); + insert_module_source( + &mut db, + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["model".to_owned()], + }, + "/main/model.solc", + r#" +import std; +export { Payload(*) }; + enum Payload { Payload(word, bool) } +"#, + ); + + let main_file = db.module_files[&key]; + assert!( + parser::parse_diagnostics(&db, main_file).is_empty(), + "{:?}", + parser::parse_diagnostics(&db, main_file) + ); + let diagnostics = diagnostics_for_module(&db, &key); + assert!( + diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("visible manual `Generic` evidence") + }), + "{diagnostics:?}" + ); +} + +#[test] +fn unsupported_std_leaf_is_not_reinterpreted_as_a_structural_user_adt() { + let (mut db, key) = db_with_main( + r#" +import std; + +contract C { + function echo(value: bytes4) public returns (word) { return 0; } +} +"#, + ); + insert_real_std_modules(&mut db); + let file = db.module_files[&key]; + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "C"); + let surface = contract_dispatch_surface(&db, module, contract); + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("standard-library type `bytes4`") + }), + "{:?}", + surface.diagnostics + ); + assert_eq!(surface.methods[0].signature, "echo()"); +} + +#[test] +fn abi_like_user_type_names_are_not_treated_as_canonical_types() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum bytes16 { bytes16(word) } + +contract C { + function echo(value: bytes16) public returns (bytes16) { return value; } +} +"#, + ); + let contract = contract_named(&db, module, "C"); + let surface = contract_dispatch_surface(&db, module, contract); + assert!( + surface.diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some("SC0231") + && diagnostic + .message + .contains("user-defined ADTs are not supported") + }), + "{:?}", + surface.diagnostics + ); + assert_eq!(surface.methods[0].signature, "echo()"); + assert_eq!(surface.methods[0].inputs[0].ty.to_string(), ""); +} + +#[test] +fn parameterized_abi_type_fails_loudly_and_duplicate_signatures_are_diagnosed() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Mapping { Mapping } + +contract Store { + function put(m: Mapping) public returns (word) { return 0; } +} +"#, + ); + let contract = contract_named(&db, module, "Store"); + let surface = contract_dispatch_surface(&db, module, contract); + assert!( + surface + .diagnostics + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0231")), + "{:?}", + surface.diagnostics + ); + assert!( + contract_abi_json(&db, module, contract) + .expect_err("unsupported ABI type") + .contains("unsupported type") + ); + assert!( + diagnostics( + r#" +enum Mapping { Mapping } + +contract Store { + function put(m: Mapping) public returns (word) { return 0; } +} +"# + ) + .iter() + .any(|diagnostic| diagnostic.code.as_deref() == Some("SC0231")) + ); + + let module = parse_module( + &db, + r#" +contract Dup { + function f(x: word) public returns (word) { return x; } + function f(x: word) public returns (word) { return x; } +} +"#, + ); + let contract = contract_named(&db, module, "Dup"); + let surface = contract_dispatch_surface(&db, module, contract); + let duplicate = surface + .diagnostics + .iter() + .find(|diagnostic| diagnostic.code.as_deref() == Some("SC0230")) + .unwrap_or_else(|| panic!("missing duplicate diagnostic: {:?}", surface.diagnostics)); + assert_eq!(duplicate.labels.len(), 2, "{duplicate:?}"); + assert!(duplicate.labels[0].is_primary(), "{duplicate:?}"); + assert!(!duplicate.labels[1].is_primary(), "{duplicate:?}"); + assert_eq!( + duplicate.labels[0].message(), + Some("duplicate ABI signature") + ); + assert_eq!(duplicate.labels[1].message(), Some("previous declaration")); +} + +#[test] +fn different_signatures_with_the_same_selector_are_diagnosed() { + let src = r#" +contract Collision { + function collision_8764(x: word) public returns () { return (); } + function collision_99992(x: word) public returns () { return (); } + function main() returns () { return (); } +} +"#; + let db = TestDb::default(); + let module = parse_module(&db, src); + let contract = contract_named(&db, module, "Collision"); + let surface = contract_dispatch_surface(&db, module, contract); + + assert_eq!(surface.methods[0].signature, "collision_8764(uint256)"); + assert_eq!(surface.methods[1].signature, "collision_99992(uint256)"); + assert_eq!(surface.methods[0].selector, surface.methods[1].selector); + assert_eq!(surface.methods[0].selector.to_hex(), "0xd443241f"); + + let collision = surface + .diagnostics + .iter() + .find(|diagnostic| { + diagnostic.code.as_deref() == Some(DiagnosticCode::TYPECK_CONTRACT_SELECTOR_COLLISION) + }) + .unwrap_or_else(|| panic!("missing selector collision: {:?}", surface.diagnostics)); + assert_eq!(collision.labels.len(), 2, "{collision:?}"); + assert!(collision.labels[0].is_primary(), "{collision:?}"); + assert!(!collision.labels[1].is_primary(), "{collision:?}"); + assert!( + collision.message.contains("collision_8764(uint256)") + && collision.message.contains("collision_99992(uint256)") + && collision.message.contains("0xd443241f"), + "{collision:?}" + ); + + let lowered = diagnostics(src); + assert!( + lowered.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some(DiagnosticCode::TYPECK_CONTRACT_SELECTOR_COLLISION) + }), + "{lowered:?}" + ); +} + +#[test] +fn frontend_desugar_plan_records_if_bool_and_storage_field_hooks() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract C { + flag: word; + + function f() public returns (word) { + if (true) { + flag = 1; + } else { + return flag; + } + } +} +"#, + ); + let plan = frontend_desugar_plan(&db, module); + let transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + + assert!( + transforms + .iter() + .any(|transform| matches!(transform, FrontendTransform::IfStmtToMatch { .. })), + "{transforms:?}" + ); + assert!( + transforms + .iter() + .any(|transform| matches!(transform, FrontendTransform::BoolToUnitSum { source, replacement, .. } if source == "true" && replacement == "inr(())")), + "{transforms:?}" + ); + assert!( + transforms + .iter() + .any(|transform| matches!(transform, FrontendTransform::FieldWrite { hook, .. } if hook.contains("LVA.acc"))), + "{transforms:?}" + ); + assert!( + transforms + .iter() + .any(|transform| matches!(transform, FrontendTransform::FieldRead { hook, .. } if hook.contains("RVA.acc"))), + "{transforms:?}" + ); +} + +#[test] +fn pre_typeck_desugar_plan_records_tuple_product_shapes_and_origins() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract C { + seed: (word, bool) =((true) ?(1, true) :(2, false)); + + function f(x: word, y: bool, z: word) public returns (word, bool, word) { + let t: (word, bool, word) = (x, y, z); + let b: bool = true; + match (b) { case true { return (x, y, z); } case false { return (z, y, x); } } + let w: word = ((y) ? x : z); + if (y) { + return (w, y, z); + } else { + return (z, y, w); + } + match (t) { case (a, b, c) { return (a, b, c); } } + } +} +"#, + ); + let plan = pre_typeck_desugar_plan(&db, module); + + assert!( + plan.types.iter().any(|transform| { + transform.origin.kind == SourceOriginKind::TupleType + && product_is_pair(&transform.product) + }), + "{:?}", + plan.types + ); + assert!( + plan.types.iter().any(|transform| { + transform.origin.kind == SourceOriginKind::TupleType + && product_is_triple(&transform.product) + }), + "{:?}", + plan.types + ); + + let body_transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + let body_types = plan + .bodies + .iter() + .flat_map(|body| body.types.iter()) + .collect::>(); + assert!( + body_types.iter().any(|transform| { + transform.origin.kind == SourceOriginKind::TupleType + && product_is_triple(&transform.product) + }), + "{body_types:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::TupleExprToProduct { + origin, + product, + .. + } if origin.kind == SourceOriginKind::TupleExpr && product_is_triple(product) + )), + "{body_transforms:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::TuplePatToProduct { + origin, + product, + .. + } if origin.kind == SourceOriginKind::TuplePat && product_is_triple(product) + )), + "{body_transforms:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::IfExprToMatch { + origin, + .. + } if origin.kind == SourceOriginKind::IfExpression + )), + "{body_transforms:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::IfStmtToMatch { + origin, + then_body, + else_body: Some(else_body), + .. + } if origin.kind == SourceOriginKind::IfStatement + && !then_body.is_empty() + && !else_body.is_empty() + )), + "{body_transforms:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::BoolToUnitSum { + origin, + value: true, + .. + } if origin.kind == SourceOriginKind::BoolConstructor + )), + "{body_transforms:?}" + ); + assert!( + body_transforms.iter().any(|transform| matches!( + transform, + PreTypeckTransform::BoolToUnitSum { + origin, + value: false, + .. + } if origin.kind == SourceOriginKind::BoolConstructor + )), + "{body_transforms:?}" + ); + + let field_init_transforms = plan + .field_inits + .iter() + .flat_map(|init| init.transforms.iter()) + .collect::>(); + assert!( + field_init_transforms.iter().any(|transform| matches!( + transform, + FieldInitPreTypeckTransform::TupleExprToProduct { + origin, + product, + .. + } if origin.kind == SourceOriginKind::TupleExpr && product_is_pair(product) + )), + "{field_init_transforms:?}" + ); + assert!( + field_init_transforms.iter().any(|transform| matches!( + transform, + FieldInitPreTypeckTransform::IfExprToMatch { + origin, + .. + } if origin.kind == SourceOriginKind::IfExpression + )), + "{field_init_transforms:?}" + ); + assert!( + field_init_transforms.iter().any(|transform| matches!( + transform, + FieldInitPreTypeckTransform::BoolToUnitSum { + origin, + value: true, + .. + } if origin.kind == SourceOriginKind::BoolConstructor + )), + "{field_init_transforms:?}" + ); + assert!( + field_init_transforms.iter().any(|transform| matches!( + transform, + FieldInitPreTypeckTransform::BoolToUnitSum { + origin, + value: false, + .. + } if origin.kind == SourceOriginKind::BoolConstructor + )), + "{field_init_transforms:?}" + ); +} + +#[test] +fn typeck_lowers_tuple_return_type_to_right_nested_product() { + let (db, key) = db_with_main( + r#" +function triple(x: word, y: bool, z: word) returns (word, bool, word) { + return (x, y, z); +} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let file = db.module_files.get(&key).copied().expect("main file"); + let module = parse_file_to_hir(&db, file).module(&db); + let function = function_named(&db, module, "triple"); + let scheme = function_scheme(&db, module_id, function.def_id_value(&db)).expect("scheme"); + let TyKind::Function { ret, .. } = scheme.body(&db).ty(&db).kind(&db) else { + panic!("expected function type"); + }; + + let outer = pair_args(&db, *ret).expect("return type is outer pair"); + assert!(matches!( + outer[0].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + .. + } + )); + let inner = pair_args(&db, outer[1]).expect("return tail is nested pair"); + assert!(matches!( + inner[0].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + .. + } + )); + assert!(matches!( + inner[1].kind(&db), + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + .. + } + )); +} + +#[test] +fn frontend_desugar_plan_records_indirect_call_shape_and_evidence() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function apply2(f: c, a: word, b: word) returns (word) where c: invokable, word> { + return f(a, b); +} +"#, + ); + let plan = frontend_desugar_plan(&db, module); + let transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + + let indirect = transforms + .iter() + .find_map(|transform| match transform { + FrontendTransform::IndirectCall { + callee, + args, + evidence, + .. + } if matches!(callee, CallSiteCallee::Invokable) && evidence.is_some() => Some(args), + _ => None, + }) + .unwrap_or_else(|| panic!("indirect call transform with evidence: {transforms:?}")); + + assert!( + matches!( + indirect, + IndirectArgShape::Pair { + tail, + .. + } if matches!(tail.as_ref(), IndirectArgShape::Single(_)) + ), + "{indirect:?}" + ); +} + +#[test] +fn frontend_desugar_plan_records_compose3_indirect_call() { + let src = + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/cases/Compose3.solc"); + assert!(diagnostics(src).is_empty()); + + let db = TestDb::default(); + let module = parse_module(&db, src); + let plan = frontend_desugar_plan(&db, module); + let transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + + assert!( + transforms.iter().any(|transform| matches!( + transform, + FrontendTransform::IndirectCall { + callee: CallSiteCallee::Invokable, + args: IndirectArgShape::Single(_), + evidence: Some(_), + .. + } + )), + "{transforms:?}" + ); +} + +#[test] +fn frontend_desugar_plan_records_simple_lambda_pair_arg_call() { + let src = + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/cases/SimpleLambda.solc"); + assert!(diagnostics(src).is_empty()); + + let db = TestDb::default(); + let module = parse_module(&db, src); + let plan = frontend_desugar_plan(&db, module); + let transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + + assert!( + transforms.iter().any(|transform| matches!( + transform, + FrontendTransform::IndirectCall { + callee: CallSiteCallee::Closure(_), + args: IndirectArgShape::Pair { tail, .. }, + evidence: Some(_), + .. + } if matches!(tail.as_ref(), IndirectArgShape::Single(_)) + )), + "{transforms:?}" + ); +} + +#[test] +fn frontend_desugar_plan_records_captured_zero_arg_closure_call() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +function inc(x: word) returns (word) { + let f = lam () { return x; }; + return f(); +} +"#, + ); + let plan = frontend_desugar_plan(&db, module); + let transforms = plan + .bodies + .iter() + .flat_map(|body| body.transforms.iter()) + .collect::>(); + + assert!( + transforms.iter().any(|transform| matches!( + transform, + FrontendTransform::IndirectCall { + callee: CallSiteCallee::Closure(_), + args: IndirectArgShape::Unit, + evidence: Some(_), + .. + } + )), + "{transforms:?}" + ); +} + +#[test] +fn derived_generic_plan_uses_right_nested_product_rep_for_tree() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +enum Tree { Leaf, Node(Tree, a, Tree) } +"#, + ); + let tree = adt_named(&db, module, "Tree"); + let plan = derived_generic_plan(&db, module, tree).expect("derived Generic plan"); + + let TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Sum), + args: sum_args, + } = plan.rep.kind(&db) + else { + panic!("expected sum rep, got {}", plan.rep.display(&db)); + }; + assert_eq!(sum_args.len(), 2); + let node_rep = sum_args[1]; + let outer_pair = pair_args(&db, node_rep).expect("Node rep is pair"); + let inner_pair = pair_args(&db, outer_pair[1]).expect("Node rep tail is pair"); + + assert!(matches!(outer_pair[0].kind(&db), TyKind::Named { .. })); + assert!(matches!(inner_pair[0].kind(&db), TyKind::BoundVar(_))); + assert!(matches!(inner_pair[1].kind(&db), TyKind::Named { .. })); + assert_eq!(plan.from_arms.len(), 2); + assert_eq!(plan.to_arms.len(), 2); +} + +#[test] +fn derived_generic_instance_plan_respects_excluded_and_manual_instances() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; +pragma solcore noGenericInstanceFor Excluded; + +trait Generic {} + +enum Eligible { Eligible(word) } +enum Excluded { Excluded(word) } +enum Manual { Manual(word) } + +impl Generic {} +"#, + ); + let generic = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ClassDef(class) => Some(class.def_id_value(&db)), + _ => None, + }) + .expect("Generic trait"); + + assert!( + derived_generic_instance_plan(&db, module, adt_named(&db, module, "Eligible"), generic) + .is_some() + ); + assert!( + derived_generic_instance_plan(&db, module, adt_named(&db, module, "Excluded"), generic) + .is_none() + ); + assert!( + derived_generic_instance_plan(&db, module, adt_named(&db, module, "Manual"), generic) + .is_none() + ); +} + +#[test] +fn interface_prototypes_are_typechecked_as_signatures_without_empty_bodies() { + let (db, key) = db_with_main( + r#" +interface Reader { + function read(key: word) external view returns (word); +} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = module_typeck_diagnostics(&db, module_id); + assert!(diagnostics.is_empty(), "{diagnostics:#?}"); +} + +#[test] +fn interface_prototype_signatures_report_type_lowering_errors() { + let (db, key) = db_with_main( + r#" +interface Reader { + function read(key: Int) external view returns (word); +} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = module_typeck_diagnostics(&db, module_id); + assert!( + diagnostics.iter().any(|diagnostic| { + let diagnostic = diagnostic.lower(&db); + diagnostic.code.as_deref() == Some("SC0229") + && diagnostic + .message + .contains("trait name used as type: `Int`") + }), + "{diagnostics:#?}" + ); +} + +#[test] +fn interface_prototypes_retain_abi_duplicate_signature_diagnostics() { + let (db, key) = db_with_main( + r#" +interface Reader { + function read(key: word) external view returns (word); + function read(key: word) external view returns (word); +} +"#, + ); + let module_id = module_id_from_key(&db, &key); + let diagnostics = module_typeck_diagnostics(&db, module_id); + assert!( + diagnostics.iter().any(|diagnostic| { + let diagnostic = diagnostic.lower(&db); + diagnostic.code.as_deref() == Some("SC0230") + && diagnostic + .message + .contains("duplicate external ABI signature in interface `Reader`") + }), + "{diagnostics:#?}" + ); +} diff --git a/crates/hir-ty/tests/fixtures/ok/comptime/class_method_runtime_body_deferred/main.solc b/crates/hir-ty/tests/fixtures/ok/comptime/class_method_runtime_body_deferred/main.solc new file mode 100644 index 00000000..0a1e9f68 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/comptime/class_method_runtime_body_deferred/main.solc @@ -0,0 +1,28 @@ +enum Box { Box(word) } + +trait Scale { + function scale(comptime factor: word, comptime x: a) returns (comptime a) ; +} + +impl Scale { + function scale(comptime factor: word, comptime x: word) returns (comptime word) { + return x; + } +} + +impl Scale { + function scale(comptime factor: word, comptime x: Box) returns (comptime Box) { + let y: word; + assembly { + y := sload(0) + } + return Box.Box(y); + } +} + +contract C { + function main() returns (word) { + let comptime a: word = Scale.scale(1, 2); + return a; + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/comptime/frontend_call_classification/main.solc b/crates/hir-ty/tests/fixtures/ok/comptime/frontend_call_classification/main.solc new file mode 100644 index 00000000..7ac5b180 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/comptime/frontend_call_classification/main.solc @@ -0,0 +1,8 @@ +function id(x: word) returns (word) { + return x; +} + +function id_ct(x: word) returns (comptime word) { + let comptime y: word = id(x); + return id(x); +} diff --git a/crates/hir-ty/tests/fixtures/ok/comptime/polymorphic_param_defers_runtime_arg/main.solc b/crates/hir-ty/tests/fixtures/ok/comptime/polymorphic_param_defers_runtime_arg/main.solc new file mode 100644 index 00000000..f26cb25d --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/comptime/polymorphic_param_defers_runtime_arg/main.solc @@ -0,0 +1,7 @@ +trait Wrap { + function unwrap(comptime x: t) returns (comptime word) ; +} + +function process(z: t) returns (word) where t: Wrap { + return Wrap.unwrap(z); +} diff --git a/crates/hir-ty/tests/fixtures/ok/comptime/return_params/main.solc b/crates/hir-ty/tests/fixtures/ok/comptime/return_params/main.solc new file mode 100644 index 00000000..1af1026c --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/comptime/return_params/main.solc @@ -0,0 +1,3 @@ +function id_ct(x: word) returns (comptime word) { + return x; +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-default-instance/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-default-instance/main.solc new file mode 100644 index 00000000..e24cf383 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-default-instance/main.solc @@ -0,0 +1,15 @@ +enum Name { Name(word) } + +trait Token { + function token(x: a) returns (word) ; +} + +default impl Token { + function token(x: a) returns (word) { + return 0; + } +} + +function main() returns (word) { + return Token.token(Name.Name(2)); +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-local-instance/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-local-instance/main.solc new file mode 100644 index 00000000..e4924285 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/p4-local-instance/main.solc @@ -0,0 +1,15 @@ +enum Wrap { Wrap(word) } + +trait Boxed { + function unbox(x: a) returns (word) ; +} + +impl Boxed { + function unbox(x: Wrap) returns (word) { + match (x) { case Wrap.Wrap(w) { return w; } } + } +} + +function main() returns (word) { + return Boxed.unbox(Wrap.Wrap(1)); +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-answer-reuse/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-answer-reuse/main.solc new file mode 100644 index 00000000..7a118515 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-answer-reuse/main.solc @@ -0,0 +1,16 @@ +pragma solcore noPattersonCondition Derived; + +trait Seed {} +trait Derived {} + +impl Seed {} + +impl Derived where a: Seed {} + +function needsDerivedTwice(x: a) returns () where a: Derived, a: Derived { + return (); +} + +function main() returns () { + return needsDerivedTwice(0); +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-given-order/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-given-order/main.solc new file mode 100644 index 00000000..23a01a3b --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-given-order/main.solc @@ -0,0 +1,23 @@ +pragma solcore noPattersonCondition C; + +trait A {} +trait B {} +trait C {} + +impl C where a: A, a: B {} + +function needsC(x: a) returns () where a: C { + return (); +} + +function fromAB(x: a) returns () where a: A, a: B { + return needsC(x); +} + +function fromBA(x: a) returns () where a: B, a: A { + return needsC(x); +} + +function main() returns () { + return (); +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-residual-given/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-residual-given/main.solc new file mode 100644 index 00000000..ccc8b737 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/local-class/tabled-residual-given/main.solc @@ -0,0 +1,18 @@ +pragma solcore noPattersonCondition Wanted; + +trait Known {} +trait Wanted {} + +impl Wanted where a: Known {} + +function needsWanted(x: a) returns () where a: Wanted { + return (); +} + +function passKnown(x: a) returns () where a: Known { + return needsWanted(x); +} + +function main() returns () { + return (); +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/00answer/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/00answer/main.solc new file mode 100644 index 00000000..48c89978 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/00answer/main.solc @@ -0,0 +1,5 @@ +contract Answer { + function main() public returns (word) { + return 42; + } +} \ No newline at end of file diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/021not/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/021not/main.solc new file mode 100644 index 00000000..d3934fc6 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/021not/main.solc @@ -0,0 +1,18 @@ +contract Not { + enum Bool { False, True } + + function main() public returns (word) { + return fromBool(bnot(Bool.False)); + } + + function fromBool(b: Bool) public returns (word) { + match (b) { + case Bool.False { return 0; } + case Bool.True { return 1; } + } + } + + function bnot(b: Bool) public returns (Bool) { + match (b) { case Bool.False { return Bool.True; } case Bool.True { return Bool.False; } } + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/022add/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/022add/main.solc new file mode 100644 index 00000000..85258483 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/022add/main.solc @@ -0,0 +1,13 @@ +function add(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +contract Add1 { + function main() public returns (word) { + return add(40, 2); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/024arith/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/024arith/main.solc new file mode 100644 index 00000000..4043007d --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/024arith/main.solc @@ -0,0 +1,64 @@ + + +function add(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +function sub(x: word, y: word) returns (word) { + let res: word; + assembly { + res := sub(x, y) + } + return res; +} + +function div(x: word, y: word) returns (word) { + let res: word; + assembly { + res := div(x, y) + } + return res; +} + +function sdiv(x: word, y: word) returns (word) { + let res: word; + assembly { + res := sdiv(x, y) + } + return res; +} + +function mod(x: word, y: word) returns (word) { + let res: word; + assembly { + res := mod(x, y) + } + return res; +} + +function smod(x: word, y: word) returns (word) { + let res: word; + assembly { + res := smod(x, y) + } + return res; +} + +function exp(x: word, y: word) returns (word) { + let res: word; + assembly { + res := exp(x, y) + } + return res; +} + + +contract Arith { + function main() public returns (word) { + return add(mod(sub(div(exp(2,18),4), 1), 16), 27); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/031maybe/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/031maybe/main.solc new file mode 100644 index 00000000..b9214087 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/031maybe/main.solc @@ -0,0 +1,13 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function main() public returns (word) { + return maybe(0, Option.Some(42)); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/036wildcard/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/036wildcard/main.solc new file mode 100644 index 00000000..f1eced27 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/036wildcard/main.solc @@ -0,0 +1,11 @@ +contract Option { + enum Option { None, Some(a) } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.Some(x) { return x; } default { return n; } } + } + + function main() public returns (word) { + return maybe(7, Option.None); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/041pair/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/041pair/main.solc new file mode 100644 index 00000000..b2581f91 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/041pair/main.solc @@ -0,0 +1,10 @@ +contract Pair { + + function fst(p: (word, word)) public returns (word) { + match (p) { case (a, b) { return a; } } + } + + function main() public returns (word) { + return fst((1,0)); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/042triple/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/042triple/main.solc new file mode 100644 index 00000000..940242b8 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/042triple/main.solc @@ -0,0 +1,10 @@ +contract Triple { + + function asel(t: (word, word, word)) public returns (word) { + match (t) { case (a, b, c) { return c; } } + } + + function main() public returns (word) { + return asel((1,21,42)); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/047rgb/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/047rgb/main.solc new file mode 100644 index 00000000..a6a0c317 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/047rgb/main.solc @@ -0,0 +1,6 @@ +contract RGB { + enum Color { R, G, B } + function main() public returns (word) { + match (Color.B) { case Color.R { return 4; } case Color.G { return 2; } case Color.B { return 42; } } + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/048rgb2/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/048rgb2/main.solc new file mode 100644 index 00000000..f7f09f48 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/048rgb2/main.solc @@ -0,0 +1,9 @@ +contract RGB { + enum Color { R, G, B } + + function fromEnum(c: Color) public returns (word) { + match (c) { case Color.R { return 4; } case Color.G { return 2; } case Color.B { return 42; } } + } + + function main() public returns (word) { return fromEnum(Color.B); } +} diff --git a/crates/hir-ty/tests/fixtures/ok/corpus/spec/049rgb3/main.solc b/crates/hir-ty/tests/fixtures/ok/corpus/spec/049rgb3/main.solc new file mode 100644 index 00000000..36b0e197 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/corpus/spec/049rgb3/main.solc @@ -0,0 +1,13 @@ +enum RGB { Red(word), Green(word), Blue(word) } + +contract RGB3 { + + function choose(c: RGB) public returns (word) { + let res: word; + match (c) { case RGB.Red(x) { assembly { res := add(x,1) } } case RGB.Green(x) { assembly { res := add(x,2) } } case RGB.Blue(x) { assembly { res := add(x,3) } } } + return res; + } + function main() public returns (word) { + return choose(RGB.Green(42)); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_bounded_variable_pragma/main.solc b/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_bounded_variable_pragma/main.solc new file mode 100644 index 00000000..f87a2254 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_bounded_variable_pragma/main.solc @@ -0,0 +1,7 @@ +pragma solcore noBoundVariableCondition Container; + +enum Box { Box(word) } +trait Eq {} +trait Container {} + +impl Container, a> where c: Eq {} diff --git a/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_patterson_pragma/main.solc b/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_patterson_pragma/main.solc new file mode 100644 index 00000000..79eb4e02 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/solver/class_scoped_patterson_pragma/main.solc @@ -0,0 +1,6 @@ +pragma solcore noPattersonCondition C1; + +trait C1 {} +trait C2 {} + +impl C1 where U: C1, U: C2 {} diff --git a/crates/hir-ty/tests/fixtures/ok/solver/global_coverage_pragma/main.solc b/crates/hir-ty/tests/fixtures/ok/solver/global_coverage_pragma/main.solc new file mode 100644 index 00000000..2f06a4a6 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/solver/global_coverage_pragma/main.solc @@ -0,0 +1,6 @@ +pragma solcore noCoverageCondition; + +enum Box { Box(word) } +trait MyClass {} + +impl MyClass, b> {} diff --git a/crates/hir-ty/tests/fixtures/ok/solver/obligation_order_improvement/main.solc b/crates/hir-ty/tests/fixtures/ok/solver/obligation_order_improvement/main.solc new file mode 100644 index 00000000..2c0de1da --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/solver/obligation_order_improvement/main.solc @@ -0,0 +1,41 @@ +// Regression test for fixpoint obligation solving with class-argument +// improvement (mirrors the reference's TcSimplify `toHnfs` fixpoint). +// +// `Assign2.assign(Mk.mk(S), 7)` pushes the callee obligation +// `?lhs:Assign2(word)` BEFORE the argument obligation `S:Mk(?o)`. A single +// in-order pass rejects the var-headed Assign2 goal (SC0207); the fixpoint +// solver defers it, solves `S:Mk(?o)` (pinning ?o := R(word) via +// class-argument unification), and then discharges the improved goal +// `R(word):Assign2(word)` in the next round. The reference compiler accepts +// this program. + +trait Assign2 { + function assign(l: lhs, r: rhs) returns () ; +} + +trait Mk { + function mk(x: s) returns (o) ; +} + +enum R { R(a) } + +impl Assign2, a> { + function assign(l: R, r: a) returns () { + return (); + } +} + +enum S { S } + +impl Mk> { + function mk(x: S) returns (R) { + return R.R(0); + } +} + +contract Main { + function main() public returns (word) { + Assign2.assign(Mk.mk(S.S), 7); + return 1; + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/abstract_data_wildcard_match/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/abstract_data_wildcard_match/main.solc new file mode 100644 index 00000000..16275b19 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/abstract_data_wildcard_match/main.solc @@ -0,0 +1,5 @@ +enum Opaque {} + +function keep(value: Opaque) returns (Opaque) { + match (value) { default { return value; } } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/bytes_storage_roundtrip_full/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/bytes_storage_roundtrip_full/main.solc new file mode 100644 index 00000000..4cf03a37 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/bytes_storage_roundtrip_full/main.solc @@ -0,0 +1,17 @@ +import std; + +contract C { + value : bytes; + + constructor(x: bytes memory) { + value = x; + } + + function get() public returns (bytes memory) { + return value; + } + + function main() returns () { + return (); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_constructor_entry_name/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_constructor_entry_name/main.solc new file mode 100644 index 00000000..9bdce770 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_constructor_entry_name/main.solc @@ -0,0 +1,8 @@ +import std; + +function init_(x: word) returns (word) { return x; } + +contract C { + constructor(x: uint256) { let saved: word = init_(Typedef.rep(x)); } + function main() returns () { return (); } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_dispatch_entry_name/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_dispatch_entry_name/main.solc new file mode 100644 index 00000000..f1b5c604 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/compiler_private_dispatch_entry_name/main.solc @@ -0,0 +1,9 @@ +import std; +import std.dispatch; + +function main(x: uint256) returns (uint256) { return x; } + +contract C { + function call_top() returns (uint256) { return main(uint256.uint256(1)); } + function ping(x: uint256) public returns (uint256) { return x; } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/compound_assignment_uses_class_method/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/compound_assignment_uses_class_method/main.solc new file mode 100644 index 00000000..11e14a4f --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/compound_assignment_uses_class_method/main.solc @@ -0,0 +1,17 @@ +trait Add { + function add(l: t, r: t) returns (t) ; +} + +enum Choice { Choice(word) } + +impl Add { + function add(l: Choice, r: Choice) returns (Choice) { + return r; + } +} + +function choose_right(x: Choice, y: Choice) returns (Choice) { + let result: Choice = x; + result += y; + return result; +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/constructor_dynamic_string_full/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/constructor_dynamic_string_full/main.solc new file mode 100644 index 00000000..66b84d03 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/constructor_dynamic_string_full/main.solc @@ -0,0 +1,17 @@ +import std; + +contract C { + value : string; + + constructor(x: string memory) { + value = x; + } + + function get() public returns (string memory) { + return value; + } + + function main() returns () { + return (); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_access/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_access/main.solc new file mode 100644 index 00000000..706e8d2f --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_access/main.solc @@ -0,0 +1,11 @@ +contract Simple { + val : word; + + function getVal() public returns (word) { + return val; + } + + function main() returns () { + return (); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_initializer/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_initializer/main.solc new file mode 100644 index 00000000..7d904e4f --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/contract_field_initializer/main.solc @@ -0,0 +1,4 @@ +contract C { + x: word = 1; + function main() returns () { return (); } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/dispatch_field_method_collision/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/dispatch_field_method_collision/main.solc new file mode 100644 index 00000000..e5a1bab9 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/dispatch_field_method_collision/main.solc @@ -0,0 +1,16 @@ +import std; +import std.dispatch; + +contract C { + enum C { Foo } + + allowance: uint256; + + function allowance() public returns (uint256) { + return allowance; + } + + function Foo() public returns (uint256) { + return 1; + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/dot_constructors_nested_patterns/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/dot_constructors_nested_patterns/main.solc new file mode 100644 index 00000000..e20fcc39 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/dot_constructors_nested_patterns/main.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(word) } + +function mkSome(x: word) returns (Option) { + return Option.Some(x); +} + +function fromOption(x: Option) returns (word) { + match (x) { case Option.Some(v) { return v; } case Option.None { return 0; } } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/generated_dispatch_explicit_imports/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/generated_dispatch_explicit_imports/main.solc new file mode 100644 index 00000000..adc92b5f --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/generated_dispatch_explicit_imports/main.solc @@ -0,0 +1,6 @@ +import std; +import std.dispatch; + +contract C { + function echo(value: uint256) public returns (uint256) { return value; } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/lib.solc b/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/lib.solc new file mode 100644 index 00000000..fe63d647 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/lib.solc @@ -0,0 +1,4 @@ +export { wrapper(wrapper), boxed(boxed) }; + +enum wrapper { wrapper(word) } +enum boxed { boxed(word) } diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/main.solc new file mode 100644 index 00000000..edb68edc --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/import_same_name_ctor_unqualified/main.solc @@ -0,0 +1,15 @@ +import {wrapper, boxed} from lib; + +// A selective import does not waive constructor qualification in either +// pattern or expression position. +function unwrap(u: wrapper) returns (word) { + match (u) { case wrapper.wrapper(w) { return w; } } +} + +function rebox(b: boxed) returns (boxed) { + match (b) { case boxed.boxed(w) { return boxed.boxed(w); } } +} + +function main() returns (word) { + return unwrap(wrapper.wrapper(3)); +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/integer_literal_pattern/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/integer_literal_pattern/main.solc new file mode 100644 index 00000000..d5c05856 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/integer_literal_pattern/main.solc @@ -0,0 +1,3 @@ +function classify(n: integer) returns (integer) { + match (n) { case 0 { return 1; } default { return n; } } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/lambda_expected_function_type/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/lambda_expected_function_type/main.solc new file mode 100644 index 00000000..b10531c2 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/lambda_expected_function_type/main.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(word) } + +function apply(f: function(word) returns (Option)) returns (Option) { + return f(1); +} + +function main() returns (Option) { + return apply(lam (x) { return Option.Some(x); }); +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/literal_poly_noclass/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/literal_poly_noclass/main.solc new file mode 100644 index 00000000..074ca455 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/literal_poly_noclass/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let y: word = 7; + return y; +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/nested_generic_adt_constructor/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/nested_generic_adt_constructor/main.solc new file mode 100644 index 00000000..6dccdf0b --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/nested_generic_adt_constructor/main.solc @@ -0,0 +1,11 @@ +contract Box { + enum Option { None, Some(u) } + + function mk(x: word) returns (Option) { + return Option.Some(x); + } + + function main() returns () { + return (); + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/qualified_and_builtin_bool_patterns/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/qualified_and_builtin_bool_patterns/main.solc new file mode 100644 index 00000000..2b40bcc9 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/qualified_and_builtin_bool_patterns/main.solc @@ -0,0 +1,13 @@ +enum flag { off, on } + +function pick(f: flag) returns (word) { + match (f) { case flag.off { return 0; } case flag.on { return 1; } } +} + +function flip(b: bool) returns (word) { + match (b) { case true { return 1; } case false { return 0; } } +} + +function main() returns (word) { + return primAddWord(pick(flag.on), flip(true)); +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/same_name_nullary_ctor_pattern/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/same_name_nullary_ctor_pattern/main.solc new file mode 100644 index 00000000..0f31f55f --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/same_name_nullary_ctor_pattern/main.solc @@ -0,0 +1,14 @@ +enum thing { thing } +enum m { m, k } + +function pickThing(t: thing) returns (word) { + match (t) { case thing.thing { return 7; } } +} + +function pickM(x: m) returns (word) { + match (x) { case m.m { return 1; } case m.k { return 2; } } +} + +function main() returns (word) { + return primAddWord(pickThing(thing.thing), pickM(m.k)); +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/self_recursive_data/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/self_recursive_data/main.solc new file mode 100644 index 00000000..cac1c5a0 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/self_recursive_data/main.solc @@ -0,0 +1,5 @@ +enum A { A(A), Z } + +function f(x: A) returns (word) { + return 0; +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_uint256/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_uint256/main.solc new file mode 100644 index 00000000..7170eee7 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_uint256/main.solc @@ -0,0 +1,24 @@ +enum mapping { mapping(word) } +enum uint256 { uint256(word) } + +trait Add { + function add(l: t, r: t) returns (t) ; +} +trait Sub { + function sub(l: t, r: t) returns (t) ; +} +impl Add { + function add(l: word, r: word) returns (word) { return l; } +} +impl Sub { + function sub(l: word, r: word) returns (word) { return l; } +} +impl Add { + function add(l: uint256, r: uint256) returns (uint256) { return l; } +} + +contract C { + m: mapping(word => uint256); + function f(k: word, v: uint256) returns () { m[k] += v; } + function main() returns () { return (); } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_word/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_word/main.solc new file mode 100644 index 00000000..8c70a4c4 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/storage_mapping_compound_add_word/main.solc @@ -0,0 +1,24 @@ +enum mapping { mapping(word) } +enum uint256 { uint256(word) } + +trait Add { + function add(l: t, r: t) returns (t) ; +} +trait Sub { + function sub(l: t, r: t) returns (t) ; +} +impl Add { + function add(l: word, r: word) returns (word) { return l; } +} +impl Sub { + function sub(l: word, r: word) returns (word) { return l; } +} +impl Add { + function add(l: uint256, r: uint256) returns (uint256) { return l; } +} + +contract C { + m: mapping(word => word); + function f(k: word) returns () { m[k] += 1; } + function main() returns () { return (); } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/storage_word_assignment_full/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/storage_word_assignment_full/main.solc new file mode 100644 index 00000000..4acad08c --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/storage_word_assignment_full/main.solc @@ -0,0 +1,29 @@ +enum storage { storage(word) } + +trait CanStore { + function store(r: a, v: b) returns () ; + function load(r: a) returns (b) ; +} + +impl CanStore { + function store(dst: word storage, src: word) returns () { + return (); + } + + function load(src: word storage) returns (word) { + return 0; + } +} + +contract StorageWordAssign { + x: word; + + function setx() returns () { + x = 8; + } + + function main() public returns (word) { + setx(); + return x; + } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/tuples_if_lambdas_for_loops_and_compound_assigns/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/tuples_if_lambdas_for_loops_and_compound_assigns/main.solc new file mode 100644 index 00000000..d1b26661 --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/tuples_if_lambdas_for_loops_and_compound_assigns/main.solc @@ -0,0 +1,87 @@ +trait Add { + function add(l: t, r: t) returns (t) ; +} + +trait Mod { + function mod(l: t, r: t) returns (t) ; +} + +trait BitAnd { + function band(l: t, r: t) returns (t) ; +} + +trait BitOr { + function bor(l: t, r: t) returns (t) ; +} + +trait BitXor { + function bxor(l: t, r: t) returns (t) ; +} + +trait Ord { + function gt(l: t, r: t) returns (bool) ; +} + +trait Eq { + function eq(l: t, r: t) returns (bool) ; +} + +impl Add { + function add(l: word, r: word) returns (word) { + return primAddWord(l, r); + } +} + +impl Mod { + function mod(l: word, r: word) returns (word) { + return l; + } +} + +impl BitAnd { + function band(l: word, r: word) returns (word) { + return l; + } +} + +impl BitOr { + function bor(l: word, r: word) returns (word) { + return l; + } +} + +impl BitXor { + function bxor(l: word, r: word) returns (word) { + return l; + } +} + +impl Ord { + function gt(l: word, r: word) returns (bool) { + return true; + } +} + +impl Eq { + function eq(l: word, r: word) returns (bool) { + return true; + } +} + +function lt(l: word, r: word) returns (bool) { + return Ord.gt(r, l); +} + +function main() returns (word) { + let f = lam (x: word) { return x; }; + let acc: word = 0; + for (let i: word = 0; i < 3; i = i + 1) { + acc += f(i); + acc ^= 1; + acc &= 7; + acc |= 2; + acc %= 5; + } + let t: (word, word) = (acc, 1); + match (t) { case (x, _) { return (x == 0 ? 1 : x); } } +} diff --git a/crates/hir-ty/tests/fixtures/ok/typeck/yul_keccak256/main.solc b/crates/hir-ty/tests/fixtures/ok/typeck/yul_keccak256/main.solc new file mode 100644 index 00000000..cd818abe --- /dev/null +++ b/crates/hir-ty/tests/fixtures/ok/typeck/yul_keccak256/main.solc @@ -0,0 +1,8 @@ +function hash_word(value: word) returns (word) { + let result: word; + assembly { + mstore(0, value) + result := keccak256(0, 32) + } + return result; +} diff --git a/crates/hir-ty/tests/frontend_smoke.rs b/crates/hir-ty/tests/frontend_smoke.rs new file mode 100644 index 00000000..ddf76a6d --- /dev/null +++ b/crates/hir-ty/tests/frontend_smoke.rs @@ -0,0 +1,1153 @@ +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + fmt::{self, Write as _}, + fs, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use hir::{ + diag::{AnyDiagnostic, DiagnosticLevel}, + input::SourceFile, +}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, module_key_for_path, module_path_display, reachable_diagnostics, + resolve_module_path_candidate, resolve_reachable_full, +}; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::Setter; +use solcore_hir_ty::infer::reachable_typeck_diagnostics; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum DiagnosticPhase { + Frontend, + Typeck, +} + +impl DiagnosticPhase { + fn as_str(self) -> &'static str { + match self { + DiagnosticPhase::Frontend => "frontend", + DiagnosticPhase::Typeck => "typeck", + } + } + + fn parse(value: &str) -> Option { + match value { + "frontend" => Some(Self::Frontend), + "typeck" => Some(Self::Typeck), + _ => None, + } + } +} + +impl fmt::Display for DiagnosticPhase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +#[derive(Clone, Copy, Debug)] +struct StdSolcKnownDivergence { + phase: DiagnosticPhase, + diagnostic_prefix: &'static str, + reason: &'static str, +} + +const STD_SOLC_KNOWN_DIVERGENCES: &[StdSolcKnownDivergence] = &[]; + +struct RunOutcome { + unresolved_imports: Vec, + frontend_diagnostics: Vec, + frontend_error_diagnostics: Vec, + frontend_has_errors: bool, + typeck_diagnostics: Vec, + typeck_error_diagnostics: Vec, + typeck_has_errors: bool, + executed: Vec, +} + +#[derive(Debug)] +struct AcceptedCorpusKnownDivergence { + phase: DiagnosticPhase, + diagnostic_prefix: String, + reason: String, +} + +struct CorpusEntry { + path: PathBuf, + main_root: PathBuf, + external_roots: BTreeMap, +} + +#[salsa::db] +#[derive(Clone)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, + executed: Arc>>, +} + +impl Default for TestDb { + fn default() -> Self { + let executed = Arc::new(Mutex::new(Vec::new())); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + }))), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: FxHashMap::default(), + executed, + } + } +} + +impl TestDb { + fn take_executed(&self) -> Vec { + std::mem::take(&mut *self.executed.lock().expect("execution log lock")) + } + + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.expect("test module tree initialized") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("test module filesystem snapshot initialized") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("test module file snapshot initialized") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl solcore_hir_ty::Db for TestDb {} + +#[test] +fn std_solc_frontend_typecheck_triage() { + let repo = repo_root(); + let corpus_root = repo.join("crates/parser/tests/fixtures/corpus/ok"); + let std_root = corpus_root.join("std"); + let outcome = run_frontend(&std_root.join("std.solc"), &std_root); + let std_triage = std_solc_triage(&outcome); + + let mut report = String::new(); + writeln!(&mut report, "std.solc frontend triage").unwrap(); + writeln!( + &mut report, + " unresolved-imports: {}", + outcome.unresolved_imports.len() + ) + .unwrap(); + writeln!( + &mut report, + " frontend-diagnostics: {}", + outcome.frontend_diagnostics.len() + ) + .unwrap(); + writeln!( + &mut report, + " typeck-diagnostics: {}", + outcome.typeck_diagnostics.len() + ) + .unwrap(); + append_diagnostic_sample(&mut report, "frontend", &outcome.frontend_diagnostics); + append_diagnostic_sample(&mut report, "typeck", &outcome.typeck_diagnostics); + append_std_solc_triage(&mut report, &std_triage); + eprintln!("{report}"); + + assert!( + outcome.unresolved_imports.is_empty(), + "std.solc has unresolved imports:\n{report}" + ); + assert!( + std_triage.unrecorded.is_empty() && std_triage.stale.is_empty(), + "{report}" + ); +} + +#[test] +fn curated_solver_files_execute_solver_and_soundness_queries() { + let repo = repo_root(); + let corpus_root = repo.join("crates/parser/tests/fixtures/corpus"); + let std_root = corpus_root.join("ok/std"); + let fixtures = [ + "examples/cases/tabled-default-instance.solc", + "examples/cases/tabled-given-order.solc", + "examples/cases/tabled-residual-given.solc", + ]; + + for fixture in fixtures { + let entry = corpus_entry(&corpus_root, fixture); + let outcome = run_frontend_with_roots( + &entry.path, + &entry.main_root, + &std_root, + entry.external_roots, + ); + let mut report = String::new(); + writeln!(&mut report, "{fixture} solver execution").unwrap(); + writeln!( + &mut report, + " unresolved-imports: {}", + outcome.unresolved_imports.len() + ) + .unwrap(); + append_diagnostic_sample(&mut report, "frontend", &outcome.frontend_diagnostics); + append_diagnostic_sample(&mut report, "typeck", &outcome.typeck_diagnostics); + writeln!( + &mut report, + " solve_report executions: {}", + query_executions(&outcome.executed, "solve_report") + ) + .unwrap(); + writeln!( + &mut report, + " instance_soundness_diagnostics executions: {}", + query_executions(&outcome.executed, "instance_soundness_diagnostics") + ) + .unwrap(); + + assert!( + outcome.unresolved_imports.is_empty() + && outcome.frontend_diagnostics.is_empty() + && outcome.typeck_diagnostics.is_empty(), + "{report}" + ); + assert!( + query_executions(&outcome.executed, "solve_report") > 0, + "{report}\n{:#?}", + outcome.executed + ); + assert!( + query_executions(&outcome.executed, "instance_soundness_diagnostics") > 0, + "{report}\n{:#?}", + outcome.executed + ); + } +} + +#[test] +fn generated_dispatch_reuses_std_instance_facts_per_module() { + let repo = repo_root(); + let entry = repo.join( + "crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty_no_constructor.solc", + ); + let std_root = repo.join("std"); + let outcome = run_frontend(&entry, &std_root); + + let module_fact_executions = query_executions(&outcome.executed, "module_instance_facts"); + let per_origin_executions = query_executions(&outcome.executed, "instance_origin_clause_set"); + let report = format!( + "generated dispatch + std impl facts\n module facts: {module_fact_executions}\n per-origin clauses: {per_origin_executions}\n frontend diagnostics: {:#?}\n typeck diagnostics: {:#?}", + outcome.frontend_diagnostics, outcome.typeck_diagnostics, + ); + eprintln!("{report}"); + + assert!(outcome.unresolved_imports.is_empty(), "{report}"); + assert!(outcome.frontend_diagnostics.is_empty(), "{report}"); + assert!(outcome.typeck_diagnostics.is_empty(), "{report}"); + assert!(module_fact_executions > 0, "{report}"); + // The canonical std surface exposes well over one hundred instance + // origins. They must be lowered by their handful of owner modules, rather + // than by one tracked query per origin. + assert!(module_fact_executions < 32, "{report}"); + assert_eq!(per_origin_executions, 0, "{report}"); +} + +#[test] +fn match_coverage_conservative_cases_emit_no_false_diagnostics() { + let repo = repo_root(); + let corpus_root = repo.join("crates/parser/tests/fixtures/corpus"); + let std_root = corpus_root.join("ok/std"); + + for fixture in [ + "examples/cases/false-redundant-warning.solc", + "examples/comptime/match_labels.solc", + "examples/cases/polymatch-error.solc", + ] { + let entry = corpus_entry(&corpus_root, fixture); + let outcome = run_frontend_with_roots( + &entry.path, + &entry.main_root, + &std_root, + entry.external_roots, + ); + assert!( + outcome.unresolved_imports.is_empty() + && outcome.frontend_error_diagnostics.is_empty() + && outcome.typeck_error_diagnostics.is_empty(), + "{fixture} failed before conservative coverage could be checked\n unresolved imports: {:#?}\n frontend errors: {:#?}\n typeck errors: {:#?}", + outcome.unresolved_imports, + outcome.frontend_error_diagnostics, + outcome.typeck_error_diagnostics, + ); + + let coverage_diagnostics = outcome + .typeck_diagnostics + .iter() + .filter(|diagnostic| { + diagnostic.starts_with("SC0302:") || diagnostic.starts_with("SC0303:") + }) + .collect::>(); + assert!( + coverage_diagnostics.is_empty(), + "{fixture} emitted false coverage diagnostics: {coverage_diagnostics:#?}" + ); + } +} + +#[test] +fn reference_rejected_corpus_stays_rejected() { + let repo = repo_root(); + let corpus_root = repo.join("crates/parser/tests/fixtures/corpus"); + let verdicts = fs::read_to_string(corpus_root.join("reference-frontend.tsv")) + .expect("reference frontend verdict manifest"); + let known_divergences = + fs::read_to_string(corpus_root.join("rust-accepted-reference-failures.tsv")) + .expect("Rust/reference reject divergence manifest"); + let mut divergence_lines = known_divergences.lines(); + assert_eq!( + divergence_lines.next(), + Some("# pathreason"), + "invalid Rust/reference divergence manifest header" + ); + let mut known_divergences = BTreeSet::new(); + for (index, line) in divergence_lines.enumerate() { + if line.is_empty() { + continue; + } + let fields = line.split('\t').collect::>(); + assert_eq!( + fields.len(), + 2, + "invalid known-divergence row {}: `{line}`", + index + 2 + ); + let (path, reason) = (fields[0], fields[1]); + assert!( + !path.is_empty(), + "missing divergence path on row {}", + index + 2 + ); + assert!( + !reason.trim().is_empty() && reason.trim() == reason, + "invalid divergence reason for `{path}`" + ); + assert!( + known_divergences.insert(path.to_owned()), + "duplicate known divergence for `{path}`" + ); + } + + let mut reference_failures = BTreeSet::new(); + let mut reference_timeouts = BTreeSet::new(); + let mut verdict_paths = BTreeSet::new(); + let mut accepted = BTreeSet::new(); + let mut verdict_lines = verdicts.lines(); + assert_eq!( + verdict_lines.next(), + Some("path\tstatus\tcode"), + "invalid reference verdict manifest header" + ); + for (index, line) in verdict_lines.enumerate() { + assert!( + !line.is_empty(), + "blank reference verdict row {}", + index + 2 + ); + let fields = line.split('\t').collect::>(); + assert_eq!( + fields.len(), + 3, + "invalid reference verdict row {}: `{line}`", + index + 2 + ); + let (path, status) = (fields[0], fields[1]); + assert!( + !path.is_empty(), + "missing reference path on row {}", + index + 2 + ); + assert!( + verdict_paths.insert(path.to_owned()), + "duplicate reference verdict for `{path}`" + ); + match status { + "pass" => continue, + "timeout" => { + reference_timeouts.insert(path.to_owned()); + continue; + } + "fail" => {} + other => panic!("invalid reference status `{other}` for `{path}`"), + } + + reference_failures.insert(path.to_owned()); + let relative = format!("examples/{path}"); + let entry = corpus_entry(&corpus_root, &relative); + assert!( + entry + .path + .starts_with(corpus_root.join("fail/test/examples")), + "reference-failed fixture `{path}` is not in the fail corpus: {}", + entry.path.display() + ); + let std_root = corpus_root.join("ok/std"); + let outcome = run_frontend_with_roots( + &entry.path, + &entry.main_root, + &std_root, + entry.external_roots, + ); + if !outcome.frontend_has_errors && !outcome.typeck_has_errors { + accepted.insert(path.to_owned()); + } + } + + let recorded_rejections = reference_failures + .union(&reference_timeouts) + .cloned() + .collect::>(); + let fail_tree = relative_solc_paths(&corpus_root.join("fail/test/examples")); + assert_eq!( + recorded_rejections, fail_tree, + "reference reject/timeout manifest and fail corpus differ" + ); + + let stale = known_divergences + .difference(&accepted) + .cloned() + .collect::>(); + let unrecorded = accepted + .difference(&known_divergences) + .cloned() + .collect::>(); + assert!( + stale.is_empty() && unrecorded.is_empty(), + "reference-rejected corpus parity changed\n unrecorded accepted files: {unrecorded:#?}\n stale known divergences: {stale:#?}" + ); +} + +#[test] +fn reference_accepted_corpus_passes_the_full_frontend() { + solcore_test_utils::run_in_large_stack(reference_accepted_corpus_passes_the_full_frontend_impl); +} + +fn reference_accepted_corpus_passes_the_full_frontend_impl() { + let repo = repo_root(); + let corpus_root = repo.join("crates/parser/tests/fixtures/corpus"); + let verdicts = fs::read_to_string(corpus_root.join("reference-frontend.tsv")) + .expect("reference frontend verdict manifest"); + let known_divergences = + fs::read_to_string(corpus_root.join("rust-rejected-reference-passes.tsv")) + .expect("Rust/reference accepted divergence manifest"); + + let mut divergence_lines = known_divergences.lines(); + assert_eq!( + divergence_lines.next(), + Some("# pathphasediagnostic-prefixreason"), + "invalid accepted-corpus divergence manifest header" + ); + let mut divergence_keys = BTreeSet::new(); + let mut divergences = BTreeMap::>::new(); + for (index, line) in divergence_lines.enumerate() { + if line.is_empty() { + continue; + } + let fields = line.split('\t').collect::>(); + assert_eq!( + fields.len(), + 4, + "invalid accepted-corpus divergence row {}: `{line}`", + index + 2 + ); + let (path, phase, diagnostic_prefix, reason) = (fields[0], fields[1], fields[2], fields[3]); + let phase = DiagnosticPhase::parse(phase).unwrap_or_else(|| { + panic!( + "invalid accepted-corpus phase `{phase}` on row {}", + index + 2 + ) + }); + assert!( + !path.is_empty(), + "missing divergence path on row {}", + index + 2 + ); + assert!( + !diagnostic_prefix.trim().is_empty() && diagnostic_prefix.trim() == diagnostic_prefix, + "invalid diagnostic prefix for `{path}`" + ); + assert!( + !reason.trim().is_empty() && reason.trim() == reason, + "invalid divergence reason for `{path}`" + ); + assert!( + divergence_keys.insert((path.to_owned(), phase, diagnostic_prefix.to_owned())), + "duplicate accepted-corpus divergence for `{path}` ({phase}, {diagnostic_prefix})" + ); + divergences + .entry(path.to_owned()) + .or_default() + .push(AcceptedCorpusKnownDivergence { + phase, + diagnostic_prefix: diagnostic_prefix.to_owned(), + reason: reason.to_owned(), + }); + } + + let mut accepted = BTreeSet::new(); + let mut verdict_lines = verdicts.lines(); + assert_eq!( + verdict_lines.next(), + Some("path\tstatus\tcode"), + "invalid reference verdict manifest header" + ); + for (index, line) in verdict_lines.enumerate() { + let fields = line.split('\t').collect::>(); + assert_eq!( + fields.len(), + 3, + "invalid reference verdict row {}: `{line}`", + index + 2 + ); + if fields[1] == "pass" { + assert!( + accepted.insert(fields[0].to_owned()), + "duplicate accepted reference verdict for `{}`", + fields[0] + ); + } + } + + let accepted_tree = relative_solc_paths(&corpus_root.join("ok/test/examples")); + assert_eq!( + accepted, accepted_tree, + "reference pass manifest and accepted corpus differ" + ); + + let std_root = corpus_root.join("ok/std"); + let mut accepted_entries = Vec::<(String, String, CorpusEntry)>::new(); + for path in &accepted { + let relative = format!("examples/{path}"); + accepted_entries.push(( + path.clone(), + format!("test/{relative}"), + corpus_entry(&corpus_root, &relative), + )); + } + for path in relative_solc_paths(&corpus_root.join("ok/test/imports")) { + let relative = format!("imports/{path}"); + accepted_entries.push(( + relative.clone(), + format!("test/{relative}"), + corpus_entry(&corpus_root, &relative), + )); + } + for path in relative_solc_paths(&std_root) { + accepted_entries.push(( + format!("std/{path}"), + format!("std/{path}"), + CorpusEntry { + path: std_root.join(&path), + main_root: std_root.clone(), + external_roots: BTreeMap::new(), + }, + )); + } + + let tested_corpus_paths = accepted_entries + .iter() + .map(|(_, corpus_path, _)| corpus_path.clone()) + .collect::>(); + let accepted_corpus_tree = relative_solc_paths(&corpus_root.join("ok")); + assert_eq!( + tested_corpus_paths, accepted_corpus_tree, + "full-frontend gate and accepted corpus tree differ" + ); + let report_paths = accepted_entries + .iter() + .map(|(report_path, _, _)| report_path.clone()) + .collect::>(); + let unknown_divergence_paths = divergences + .keys() + .filter(|path| !report_paths.contains(*path)) + .cloned() + .collect::>(); + assert!( + unknown_divergence_paths.is_empty(), + "accepted-corpus divergence manifest contains non-pass files: {unknown_divergence_paths:#?}" + ); + + let mut unrecorded = Vec::new(); + let mut stale = Vec::new(); + let mut grouped_entries = BTreeMap::>::new(); + for (path, _, entry) in accepted_entries { + grouped_entries + .entry(entry.main_root.clone()) + .or_default() + .push((path, entry)); + } + + // Reuse one Salsa database per corpus root. Preloading the entry inputs + // keeps the module-file snapshot stable after the first std import graph is + // loaded, so this remains a broad regression gate without rechecking std + // from scratch for every accepted fixture. + for (main_root, entries) in grouped_entries { + let external_roots = entries + .first() + .expect("accepted corpus group is nonempty") + .1 + .external_roots + .clone(); + assert!( + entries + .iter() + .all(|(_, entry)| entry.external_roots == external_roots), + "fixtures under {} disagree on external roots", + main_root.display() + ); + let mut db = test_db_for_roots(&main_root, &std_root, external_roots); + for (_, entry) in &entries { + insert_entry_source(&mut db, &entry.path, &main_root); + } + + for (path, entry) in entries { + let outcome = run_frontend_in_db(&mut db, &entry.path, &main_root); + let actual = outcome + .frontend_error_diagnostics + .iter() + .map(|diagnostic| (DiagnosticPhase::Frontend, diagnostic)) + .chain( + outcome + .typeck_error_diagnostics + .iter() + .map(|diagnostic| (DiagnosticPhase::Typeck, diagnostic)), + ) + .collect::>(); + let known = divergences + .get(&path) + .map(Vec::as_slice) + .unwrap_or_default(); + + for (phase, diagnostic) in &actual { + if !known.iter().any(|divergence| { + divergence.phase == *phase + && diagnostic.starts_with(&divergence.diagnostic_prefix) + }) { + unrecorded.push(format!("{path}\t{phase}\t{diagnostic}")); + } + } + for divergence in known { + if !actual.iter().any(|(phase, diagnostic)| { + *phase == divergence.phase + && diagnostic.starts_with(&divergence.diagnostic_prefix) + }) { + stale.push(format!( + "{path}\t{}\t{}\t{}", + divergence.phase, divergence.diagnostic_prefix, divergence.reason + )); + } + } + } + } + + assert!( + unrecorded.is_empty() && stale.is_empty(), + "reference-accepted full-frontend parity changed\n unrecorded errors: {unrecorded:#?}\n stale known divergences: {stale:#?}" + ); +} + +fn relative_solc_paths(root: &Path) -> BTreeSet { + fn walk(root: &Path, directory: &Path, paths: &mut BTreeSet) { + let entries = fs::read_dir(directory) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display())); + for entry in entries { + let entry = entry.unwrap_or_else(|error| { + panic!( + "failed to read an entry under {}: {error}", + directory.display() + ) + }); + let path = entry.path(); + if path.is_dir() { + walk(root, &path, paths); + } else if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + let relative = path + .strip_prefix(root) + .expect("walked path is below corpus root") + .iter() + .map(|component| component.to_str().expect("UTF-8 corpus path")) + .collect::>() + .join("/"); + assert!( + paths.insert(relative.clone()), + "duplicate corpus path `{relative}`" + ); + } + } + } + + let mut paths = BTreeSet::new(); + walk(root, root, &mut paths); + paths +} + +fn corpus_entry(corpus_root: &Path, relative: &str) -> CorpusEntry { + for status in ["ok", "fail", "known-diagnostic-gaps"] { + let test_root = corpus_root.join(status).join("test"); + let path = test_root.join(relative); + if path.exists() { + let main_root = main_root_for_fixture(&test_root, relative); + let mut external_roots = BTreeMap::new(); + if relative.starts_with("imports/") { + external_roots.insert("extlib".to_owned(), test_root.join("imports/extlib")); + } + return CorpusEntry { + path, + main_root, + external_roots, + }; + } + } + panic!("expectation fixture `{relative}` does not exist in corpus"); +} + +fn main_root_for_fixture(test_root: &Path, relative: &str) -> PathBuf { + if relative.starts_with("diagnostics/") { + test_root.join("diagnostics") + } else if relative.starts_with("examples/cases/") { + test_root.join("examples/cases") + } else if relative.starts_with("examples/comptime/") { + test_root.join("examples/comptime") + } else if relative.starts_with("examples/dispatch/") { + test_root.join("examples/dispatch") + } else if relative.starts_with("examples/invokable/") { + test_root.join("examples/invokable") + } else if relative.starts_with("examples/opcodes/") { + test_root.join("examples/opcodes") + } else if relative.starts_with("examples/pragmas/") { + test_root.join("examples/pragmas") + } else if relative.starts_with("examples/spec/") { + test_root.join("examples/spec") + } else if relative.starts_with("examples/") { + test_root.join("examples") + } else if relative.starts_with("imports/extlib/") { + test_root.join("imports/extlib") + } else if relative.starts_with("imports/") { + test_root.join("imports") + } else { + panic!("unknown corpus fixture area `{relative}`"); + } +} +fn run_frontend(path: &Path, std_root: &Path) -> RunOutcome { + let main_root = path + .parent() + .expect("entry path has a parent directory") + .to_path_buf(); + run_frontend_with_roots(path, &main_root, std_root, BTreeMap::new()) +} + +fn run_frontend_with_roots( + path: &Path, + main_root: &Path, + std_root: &Path, + external_roots: BTreeMap, +) -> RunOutcome { + let mut db = test_db_for_roots(main_root, std_root, external_roots); + run_frontend_in_db(&mut db, path, main_root) +} + +fn test_db_for_roots( + main_root: &Path, + std_root: &Path, + external_roots: BTreeMap, +) -> TestDb { + let mut db = TestDb::default(); + db.module_tree = Some(ModuleTree::new( + &db, + main_root.to_path_buf(), + std_root.to_path_buf(), + external_roots.clone(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + &db, + std::iter::once(main_root) + .chain(std::iter::once(std_root)) + .chain(external_roots.values().map(|path| path.as_path())), + )); + db +} + +fn insert_entry_source(db: &mut TestDb, path: &Path, main_root: &Path) -> ModuleKey { + let entry_key = module_key_for_path(LibraryId::Main, main_root, path) + .expect("entry file is under its main root"); + if !db.module_files.contains_key(&entry_key) { + let source = fs::read_to_string(path).expect("fixture source"); + let entry_file = source_file_for_path(db, path, source); + db.insert_module_file(entry_key.clone(), entry_file); + } + entry_key +} + +fn run_frontend_in_db(db: &mut TestDb, path: &Path, main_root: &Path) -> RunOutcome { + let entry_key = insert_entry_source(db, path, main_root); + + let unresolved_imports = load_reachable_modules(db, entry_key.clone()); + let entry = module_id_from_key(&*db, &entry_key); + let _ = db.take_executed(); + let _ = resolve_reachable_full(&*db, entry); + let reachable_frontend = reachable_diagnostics(&*db, entry); + let mut frontend_error_diagnostics = summarize_error_diagnostics(&*db, reachable_frontend); + frontend_error_diagnostics.extend( + unresolved_imports + .iter() + .map(|unresolved| format!("unresolved-import: {unresolved}")), + ); + frontend_error_diagnostics.sort(); + frontend_error_diagnostics.dedup(); + let frontend_has_errors = !frontend_error_diagnostics.is_empty(); + let mut frontend_diagnostics = summarize_diagnostics(&*db, reachable_frontend); + frontend_diagnostics.extend( + unresolved_imports + .iter() + .map(|unresolved| format!("unresolved-import: {unresolved}")), + ); + frontend_diagnostics.sort(); + frontend_diagnostics.dedup(); + let reachable_typeck = reachable_typeck_diagnostics(&*db, entry); + let typeck_error_diagnostics = summarize_error_diagnostics(&*db, reachable_typeck); + let typeck_has_errors = !typeck_error_diagnostics.is_empty(); + let typeck_diagnostics = summarize_diagnostics(&*db, reachable_typeck); + let executed = db.take_executed(); + + RunOutcome { + unresolved_imports, + frontend_diagnostics, + frontend_error_diagnostics, + frontend_has_errors, + typeck_diagnostics, + typeck_error_diagnostics, + typeck_has_errors, + executed, + } +} + +fn load_reachable_modules(db: &mut TestDb, entry: ModuleKey) -> Vec { + let mut queue = VecDeque::from([entry]); + let mut visited = FxHashSet::default(); + let mut unresolved = Vec::new(); + + while let Some(key) = queue.pop_front() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = db.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map( + |path| match resolve_module_path_candidate(&*db, module, &path) { + Ok(resolved) => Some((resolved.module.key(&*db), resolved.file_path)), + Err(_) => { + unresolved.push(format!( + "{} imports `{}`", + module.display(&*db), + module_path_display(&*db, &path) + )); + None + } + }, + ) + .collect::>() + }; + for (target_key, file_path) in targets { + if !db.module_files.contains_key(&target_key) { + match fs::read_to_string(&file_path) { + Ok(source) => { + let file = source_file_for_path(db, &file_path, source); + db.insert_module_file(target_key.clone(), file); + } + Err(err) => unresolved.push(format!( + "failed to read {} for {}: {err}", + file_path.display(), + module_key_display(&target_key) + )), + } + } + if db.module_files.contains_key(&target_key) { + queue.push_back(target_key); + } + } + } + + unresolved.sort(); + unresolved.dedup(); + unresolved +} + +fn module_fs_snapshot_for_roots<'a>( + db: &TestDb, + roots: impl IntoIterator, +) -> ModuleFsSnapshot { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +fn source_file_for_path(db: &TestDb, path: &Path, source: String) -> SourceFile { + let url = url::Url::from_file_path(path).expect("file URL"); + SourceFile::new(db, url, Some(source)) +} + +fn summarize_diagnostics(db: &dyn hir::Db, diagnostics: &[AnyDiagnostic]) -> Vec { + let mut summaries = diagnostics + .iter() + .map(|diagnostic| { + let diagnostic = diagnostic.lower(db); + let code = diagnostic.code.as_deref().unwrap_or("no-code"); + format!("{code}: {}", diagnostic.message) + }) + .collect::>(); + summaries.sort(); + summaries.dedup(); + summaries +} + +fn summarize_error_diagnostics(db: &dyn hir::Db, diagnostics: &[AnyDiagnostic]) -> Vec { + let errors = diagnostics + .iter() + .filter(|diagnostic| diagnostic.lower(db).level == DiagnosticLevel::Error) + .cloned() + .collect::>(); + summarize_diagnostics(db, &errors) +} +#[derive(Default)] +struct StdSolcTriage { + known_by_reason: BTreeMap<&'static str, Vec>, + unrecorded: Vec, + stale: Vec<&'static StdSolcKnownDivergence>, +} + +struct StdSolcDiagnostic { + phase: DiagnosticPhase, + diagnostic: String, +} + +fn std_solc_triage(outcome: &RunOutcome) -> StdSolcTriage { + let mut triage = StdSolcTriage::default(); + let mut seen = BTreeSet::<(DiagnosticPhase, &'static str)>::new(); + for (phase, diagnostic) in outcome + .frontend_diagnostics + .iter() + .map(|diagnostic| (DiagnosticPhase::Frontend, diagnostic)) + .chain( + outcome + .typeck_diagnostics + .iter() + .map(|diagnostic| (DiagnosticPhase::Typeck, diagnostic)), + ) + { + if let Some(known) = std_solc_known_divergence(phase, diagnostic) { + seen.insert((known.phase, known.diagnostic_prefix)); + triage + .known_by_reason + .entry(known.reason) + .or_default() + .push(format!("{phase}: {diagnostic}")); + } else { + triage.unrecorded.push(StdSolcDiagnostic { + phase, + diagnostic: diagnostic.clone(), + }); + } + } + triage.stale = STD_SOLC_KNOWN_DIVERGENCES + .iter() + .filter(|known| !seen.contains(&(known.phase, known.diagnostic_prefix))) + .collect(); + triage +} + +fn std_solc_known_divergence( + phase: DiagnosticPhase, + diagnostic: &str, +) -> Option<&'static StdSolcKnownDivergence> { + STD_SOLC_KNOWN_DIVERGENCES + .iter() + .find(|known| known.phase == phase && diagnostic.starts_with(known.diagnostic_prefix)) +} +fn append_diagnostic_sample(report: &mut String, label: &str, diagnostics: &[String]) { + if diagnostics.is_empty() { + return; + } + writeln!(report, " {label}:").unwrap(); + for diagnostic in diagnostics.iter().take(3) { + writeln!(report, " {diagnostic}").unwrap(); + } + if diagnostics.len() > 3 { + writeln!(report, " ... {} more", diagnostics.len() - 3).unwrap(); + } +} + +fn append_std_solc_triage(report: &mut String, triage: &StdSolcTriage) { + if !triage.known_by_reason.is_empty() { + writeln!(report, "\nstd.solc known diagnostic families").unwrap(); + for (reason, diagnostics) in &triage.known_by_reason { + writeln!(report, " {reason}: {}", diagnostics.len()).unwrap(); + for diagnostic in diagnostics.iter().take(6) { + writeln!(report, " {diagnostic}").unwrap(); + } + if diagnostics.len() > 6 { + writeln!(report, " ... {} more", diagnostics.len() - 6).unwrap(); + } + } + } + + if !triage.unrecorded.is_empty() { + writeln!(report, "\nstd.solc unrecorded diagnostic families").unwrap(); + for diagnostic in triage.unrecorded.iter().take(20) { + writeln!(report, " {}: {}", diagnostic.phase, diagnostic.diagnostic).unwrap(); + } + if triage.unrecorded.len() > 20 { + writeln!( + report, + " ... {} more unrecorded std.solc diagnostics", + triage.unrecorded.len() - 20 + ) + .unwrap(); + } + } + + if !triage.stale.is_empty() { + writeln!(report, "\nstd.solc stale diagnostic families").unwrap(); + for known in &triage.stale { + writeln!( + report, + " {} {} ({})", + known.phase, known.diagnostic_prefix, known.reason + ) + .unwrap(); + } + } +} + +fn query_executions(events: &[String], query: &str) -> usize { + events.iter().filter(|event| event.contains(query)).count() +} + +fn module_key_display(key: &ModuleKey) -> String { + let path = key.logical_path.join("."); + match &key.library { + LibraryId::Main => path, + LibraryId::Std if key.logical_path.as_slice() == ["std"] => "std".to_owned(), + LibraryId::Std => format!("std.{path}"), + LibraryId::External(name) => format!("@{name}.{path}"), + } +} + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("hir-ty crate lives under /crates/hir-ty") + .to_path_buf() +} diff --git a/crates/hir-ty/tests/incremental_cache.rs b/crates/hir-ty/tests/incremental_cache.rs new file mode 100644 index 00000000..7afad66a --- /dev/null +++ b/crates/hir-ty/tests/incremental_cache.rs @@ -0,0 +1,488 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use hir::{ + ast::item::{ContractDef, Item, Module}, + input::SourceFile, +}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_diagnostics, module_id_from_key, +}; +use parser::parse_file_to_hir; +use rustc_hash::FxHashMap; +use salsa::Setter; +use solcore_hir_ty::{contract_dispatch_surface, infer::module_typeck_diagnostics}; + +#[salsa::db] +#[derive(Clone)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, + executed: Arc>>, +} + +impl Default for TestDb { + fn default() -> Self { + let executed = Arc::new(Mutex::new(Vec::new())); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + }))), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: FxHashMap::default(), + executed, + } + } +} + +impl TestDb { + fn take_executed(&self) -> Vec { + std::mem::take(&mut *self.executed.lock().expect("execution log lock")) + } + + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.expect("test module tree initialized") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("test module filesystem snapshot initialized") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("test module file snapshot initialized") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl solcore_hir_ty::Db for TestDb {} + +#[test] +fn unrelated_signature_edit_does_not_rerun_every_body_inference() { + let before = r#" +function id(x: word) returns (word) { return x as word; } +function unrelated(x: word) returns (word) { return 0; } +function main() returns (word) { return id(1); } +"#; + let after = r#" +function id(x: word) returns (word) { return x as word; } +function unrelated(x: bool) returns (word) { return 0; } +function main() returns (word) { return id(1); } +"#; + let (mut db, file, key) = db_with_main(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "infer_body"), + 3, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "infer_body"), + 1, + "{executed:#?}" + ); + } +} + +#[test] +fn same_obligation_body_edit_does_not_resolve_solver_query() { + let before = r#" +trait C {} +impl C {} +function use(x: a) returns (word) where a: C { return 0; } + +function main() returns (word) { + let y: word = 1; + return use(1); +} +"#; + let after = r#" +trait C {} +impl C {} +function use(x: a) returns (word) where a: C { return 0; } + +function main() returns (word) { + let y: word = 2; + return use(1); +} +"#; + let (mut db, file, key) = db_with_main(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert!( + query_executions(&executed, "solve_report") > 0, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "infer_body"), + 1, + "{executed:#?}" + ); + assert_eq!( + query_executions(&executed, "solve_report"), + 0, + "{executed:#?}" + ); + } +} + +#[test] +fn instance_soundness_edit_is_backdated_into_module_diagnostics() { + let before = r#" +enum Box { Box(word) } +trait C {} +impl C, b> {} +"#; + let after = r#" +enum Box { Box(word) } +trait C {} +impl C, word> {} +"#; + let (mut db, file, key) = db_with_main(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let diagnostics = module_typeck_diagnostics(&db, module); + assert!( + !diagnostics.is_empty(), + "expected coverage diagnostic before edit" + ); + let executed = db.take_executed(); + assert!( + query_executions(&executed, "instance_soundness_diagnostics") > 0, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let diagnostics = module_typeck_diagnostics(&db, module); + assert!(diagnostics.is_empty(), "{diagnostics:?}"); + let executed = db.take_executed(); + assert!( + query_executions(&executed, "instance_soundness_diagnostics") > 0, + "{executed:#?}" + ); + } +} + +#[test] +fn contract_body_edit_does_not_rerun_dispatch_surface_query() { + let before = r#" +contract C { + function get() public returns (word) { return 1; } +} +"#; + let after = r#" +contract C { + function get() public returns (word) { return 2; } +} +"#; + let (mut db, file, _key) = db_with_main(before); + + { + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "C"); + let _ = db.take_executed(); + let surface = contract_dispatch_surface(&db, module, contract); + assert_eq!(surface.methods.len(), 1); + let executed = db.take_executed(); + assert!( + query_executions(&executed, "contract_dispatch_surface") > 0, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = parse_file_to_hir(&db, file).module(&db); + let contract = contract_named(&db, module, "C"); + let _ = db.take_executed(); + let surface = contract_dispatch_surface(&db, module, contract); + assert_eq!(surface.methods.len(), 1); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "contract_dispatch_surface"), + 0, + "{executed:#?}" + ); + } +} + +#[test] +fn import_diagnostic_span_edit_does_not_rerun_unrelated_body_inference() { + let before = concat!( + "\n", + "import {f} from util; \x20\n", + "function f() returns (word) { return 1; }\n", + "function main() returns (word) { return f(); }\n", + ); + let after = r#" +import {f} from util; +function f() returns (word) { return 1; } +function main() returns (word) { return f(); } +"#; + let (mut db, file, key) = db_with_selected_import_conflict(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert_eq!(diagnostic_count(&db, module, "SC0108"), 1); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "infer_body"), + 2, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert_eq!(diagnostic_count(&db, module, "SC0108"), 1); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "infer_body"), + 0, + "{executed:#?}" + ); + } +} + +#[test] +fn desugar_body_edit_does_not_rerun_unrelated_body_inference() { + let before = r#" +function choose(b: bool, x: word, y: word) returns (word) { + let p: (word, bool) = (x, true); + let selected: word = (b ? x : y); + match (p) { case (head, flag) { return selected; } } +} + +function stable(x: word) returns (word) { return x; } +function main() returns (word) { return stable(choose(false, 1, 2)); } +"#; + let after = r#" +function choose(b: bool, x: word, y: word) returns (word) { + let p: (word, bool) = (x, false); + let selected: word = (b ? x : y); + match (p) { case (head, flag) { return selected; } } +} + +function stable(x: word) returns (word) { return x; } +function main() returns (word) { return stable(choose(false, 1, 2)); } +"#; + let (mut db, file, key) = db_with_main(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert!( + query_executions(&executed, "pre_typeck_desugar_body_tree") > 0, + "{executed:#?}" + ); + assert_eq!( + query_executions(&executed, "infer_body"), + 3, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_typeck_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "pre_typeck_desugar_body_tree"), + 1, + "{executed:#?}" + ); + assert_eq!( + query_executions(&executed, "infer_body"), + 1, + "{executed:#?}" + ); + } +} + +fn db_with_main(content: &str) -> (TestDb, SourceFile, ModuleKey) { + let mut db = TestDb::default(); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory"), + PathBuf::from("/memory/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(ModuleFsSnapshot::new(&db, BTreeSet::new(), BTreeMap::new())); + let file = SourceFile::new( + &db, + "memory:///main.solc".parse().expect("valid URL"), + Some(content.to_owned()), + ); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + db.insert_module_file(key.clone(), file); + (db, file, key) +} + +fn db_with_selected_import_conflict(content: &str) -> (TestDb, SourceFile, ModuleKey) { + let mut db = TestDb::default(); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory"), + PathBuf::from("/memory/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(ModuleFsSnapshot::new(&db, BTreeSet::new(), BTreeMap::new())); + + let util_key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["util".to_owned()], + }; + let util_file = SourceFile::new( + &db, + "memory:///util.solc".parse().expect("valid URL"), + Some("function f() returns (word) { return 0; }\nexport { f };\n".to_owned()), + ); + db.insert_module_file(util_key, util_file); + + let file = SourceFile::new( + &db, + "memory:///main.solc".parse().expect("valid URL"), + Some(content.to_owned()), + ); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + db.insert_module_file(key.clone(), file); + (db, file, key) +} + +fn diagnostic_count(db: &TestDb, module: ModuleId<'_>, code: &str) -> usize { + module_diagnostics(db, module) + .iter() + .filter(|diagnostic| diagnostic.lower(db).code.as_deref() == Some(code)) + .count() +} + +fn contract_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> ContractDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) + if contract.def_id_value(db).name(db).as_deref() == Some(name) => + { + Some(*contract) + } + _ => None, + }) + .expect("contract") +} + +fn query_executions(events: &[String], query: &str) -> usize { + events.iter().filter(|event| event.contains(query)).count() +} diff --git a/crates/hir-ty/tests/ok_fixtures.rs b/crates/hir-ty/tests/ok_fixtures.rs new file mode 100644 index 00000000..3a38f829 --- /dev/null +++ b/crates/hir-ty/tests/ok_fixtures.rs @@ -0,0 +1,46 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use dir_test::{Fixture, dir_test}; +use hir::diag::Diagnostic; +use nameres::{ModuleKey, module_id_from_key}; +use solcore_test_utils::{ + define_frontend_test_db, load_fixture_case, load_reachable_modules, lower_any_diagnostics, + render_diagnostics, repo_root_from_manifest, run_in_large_stack, +}; + +define_frontend_test_db!(TestDb, solcore_hir_ty); + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/ok", + glob: "**/main.solc" +)] +fn hir_ty_ok_fixture_has_no_diagnostics(fixture: Fixture<&str>) { + let case_dir = PathBuf::from(fixture.path()) + .parent() + .expect("case dir") + .to_path_buf(); + run_in_large_stack(move || { + let repo_root = repo_root_from_manifest(env!("CARGO_MANIFEST_DIR")); + let mut db = TestDb::default(); + let entry = load_fixture_case(&mut db, &case_dir, &repo_root, BTreeMap::new()); + load_reachable_modules(&mut db, entry.clone()); + let diagnostics = full_frontend_diagnostics(&db, entry); + assert!( + diagnostics.is_empty(), + "expected no diagnostics for OK fixture `{}`\n{}", + case_dir.display(), + render_diagnostics(&db, &diagnostics) + ); + }); +} + +fn full_frontend_diagnostics(db: &TestDb, entry: ModuleKey) -> Vec { + let entry = module_id_from_key(db, &entry); + let mut diagnostics = nameres::reachable_diagnostics(db, entry).to_vec(); + diagnostics.extend( + solcore_hir_ty::infer::reachable_typeck_diagnostics(db, entry) + .iter() + .cloned(), + ); + lower_any_diagnostics(db, diagnostics) +} diff --git a/crates/hir-ty/tests/properties.rs b/crates/hir-ty/tests/properties.rs new file mode 100644 index 00000000..d43ffb10 --- /dev/null +++ b/crates/hir-ty/tests/properties.rs @@ -0,0 +1,71 @@ +use proptest::prelude::*; +use solcore_hir_ty as hir_ty; +use solcore_test_utils::{define_frontend_test_db, load_main_source}; + +define_frontend_test_db!(TestDb, hir_ty); + +fn run_frontend(source: &str) { + let mut db = TestDb::default(); + let entry = load_main_source(&mut db, source); + let entry = nameres::module_id_from_key(&db, &entry); + let _ = nameres::reachable_diagnostics(&db, entry); + let _ = hir_ty::infer::reachable_typeck_diagnostics(&db, entry); +} + +fn generated_program(literal: u64, depth: usize, result_kind: u8) -> String { + let mut source = + format!("function main(value: word) returns (word) {{\n let value0: word = {literal};\n"); + for index in 1..=depth { + source.push_str(&format!(" let value{index}: word = value{};\n", index - 1)); + } + let result = match result_kind { + 0 => format!("value{depth}"), + 1 => "true".to_owned(), + 2 => "missing".to_owned(), + _ => format!("(true ? value : value{depth})"), + }; + source.push_str(&format!(" return {result};\n}}\n")); + source +} + +#[test] +fn frontend_test_db_invalidates_a_query_started_before_file_seeding() { + let mut db = TestDb::default(); + let key = nameres::ModuleKey { + library: nameres::LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + { + let module = nameres::module_id_from_key(&db, &key); + assert!(nameres::module_diagnostics(&db, module).is_empty()); + } + + let loaded = load_main_source(&mut db, "function main( {\n"); + assert_eq!(loaded, key); + let module = nameres::module_id_from_key(&db, &key); + assert!( + !nameres::module_diagnostics(&db, module).is_empty(), + "the pre-seed empty result must be invalidated when the file snapshot changes" + ); +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + + #[test] + fn arbitrary_utf8_never_panics_at_the_frontend_boundary( + source in prop::collection::vec(any::(), 0..256) + .prop_map(|characters| characters.into_iter().collect::()), + ) { + run_frontend(&source); + } + + #[test] + fn generated_parse_clean_programs_never_panic_in_nameres_or_typeck( + literal in any::(), + depth in 0usize..32, + result_kind in 0u8..4, + ) { + run_frontend(&generated_program(literal, depth, result_kind)); + } +} diff --git a/crates/hir-ty/tests/scheme_cycle.rs b/crates/hir-ty/tests/scheme_cycle.rs new file mode 100644 index 00000000..1eb53746 --- /dev/null +++ b/crates/hir-ty/tests/scheme_cycle.rs @@ -0,0 +1,22 @@ +//! Regression tests for divergent signature-inference fixpoints. + +use solcore_hir_ty as hir_ty; +use solcore_test_utils::{define_frontend_test_db, load_main_source, run_in_large_stack}; + +define_frontend_test_db!(TestDb, hir_ty); + +/// `return f` makes `f`'s inferred signature grow every fixpoint round; the +/// scheme query must converge through its cycle fallback instead of Salsa +/// panicking with "too many cycle iterations". The program is currently still +/// accepted under legacy signature inference (the reference meanwhile rejects +/// it with SC0220 "incomplete signature"), so only panic-freedom is asserted. +#[test] +fn divergent_recursive_signature_does_not_panic() { + run_in_large_stack(|| { + let mut db = TestDb::default(); + let entry = load_main_source(&mut db, "function f(x: word) {\n return f;\n}\n"); + let entry = nameres::module_id_from_key(&db, &entry); + let _ = nameres::reachable_diagnostics(&db, entry); + let _ = hir_ty::infer::reachable_typeck_diagnostics(&db, entry); + }); +} diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml index 7b1c17d5..e7407d9c 100644 --- a/crates/hir/Cargo.toml +++ b/crates/hir/Cargo.toml @@ -2,8 +2,12 @@ name = "solcore-hir" version = "0.1.0" edition.workspace = true +rust-version.workspace = true [dependencies] salsa = { workspace = true } annotate-snippets = { workspace = true } +percent-encoding = { workspace = true } +rustc-hash = { workspace = true } url = { workspace = true } +tracing = { workspace = true } diff --git a/crates/hir/src/anchor.rs b/crates/hir/src/anchor.rs index 2beb18aa..24d65a98 100644 --- a/crates/hir/src/anchor.rs +++ b/crates/hir/src/anchor.rs @@ -1,8 +1,27 @@ +//! Stable structural identity for HIR definitions. +//! +//! [`crate::anchor::DefId`] is the identity used by semantic phases, spans, and +//! diagnostics to refer to definitions across Salsa revisions. A definition key +//! is structural: it contains the source file, an owner chain, a +//! [`crate::anchor::DefKind`], an optional surface name, an optional structural +//! fingerprint, and a disambiguator. +//! +//! The owner chain is the primary nesting model. A method belongs to its +//! instance or contract, and a function body belongs to its function, so moving +//! unrelated sibling text should not change the identity of nested definitions. +//! Fingerprints are reserved for definitions whose surface name is not enough +//! to describe identity, such as selected imports, exports, or instance heads. +//! The disambiguator is deliberately last-resort and allocation-order based: it +//! should be non-zero only when otherwise identical base keys occur more than +//! once in the same owner. + use std::{ - collections::HashMap, + fmt, hash::{DefaultHasher, Hash, Hasher}, }; +use rustc_hash::FxHashMap; + use crate::{diag::Offset, input::SourceFile}; /// Disambiguator for defs/bodies sharing the same canonical base key. @@ -10,30 +29,56 @@ use crate::{diag::Offset, input::SourceFile}; pub struct Disambiguator(u32); impl Disambiguator { + /// The first occurrence of a canonical base key. + /// + /// Most well-formed definitions use this value. Higher values indicate + /// duplicate structural keys, not separate semantic meaning. pub const ZERO: Self = Self(0); + /// Creates a disambiguator from its raw ordinal. pub const fn new(raw: u32) -> Self { Self(raw) } + /// Returns the raw duplicate ordinal. pub const fn as_u32(self) -> u32 { self.0 } } +/// Coarse kind of HIR definition represented by a [`DefId`]. +/// +/// The kind is part of structural identity so same-named functions, types, and +/// bodies do not collide under one owner. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum DefKind { + /// Synthetic definition for a lowered module/file. Module, + /// Function, constructor, fallback, or method signature/body owner. Function, + /// Function body arena, including nested lambda bodies. FuncBody, + /// Type alias declaration. TypeAlias, + /// User-defined value type declaration. + ValueType, + /// Algebraic data type declaration. Adt, + /// Algebraic data constructor. AdtCtor, + /// Type class declaration. Class, + /// Type class instance declaration. Instance, + /// Contract declaration. Contract, + /// Contract field declaration. Field, + /// Import declaration. Import, + /// Export declaration. + Export, + /// Pragma declaration. Pragma, } @@ -41,17 +86,32 @@ pub enum DefKind { #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub(crate) struct DefKey { pub(crate) file: SourceFile, + pub(crate) owner: Option>, pub(crate) kind: DefKind, pub(crate) name: Option, + pub(crate) fingerprint: Option, pub(crate) disambiguator: Disambiguator, } /// Canonical definition key. +/// +/// `DefId` is interned from a structural key rather than allocated from a +/// global counter. The identity is stable when byte positions shift, provided +/// the owner chain, kind, name, fingerprint, and duplicate ordinal stay the +/// same. #[salsa::interned(debug)] pub struct DefId<'db> { + /// Source file that owns this definition's structural key. pub file: SourceFile, + /// Lexical/semantic owner, or `None` for the module root. + pub owner: Option>, + /// Category of definition this key represents. pub kind: DefKind, + /// Surface name when the syntax has one. pub name: Option, + /// Structural identity supplement for name-insufficient definitions. + pub fingerprint: Option, + /// Duplicate ordinal for otherwise identical keys under one owner. pub disambiguator: Disambiguator, } @@ -59,30 +119,59 @@ impl<'db> DefId<'db> { pub(crate) fn key(self, db: &'db dyn crate::Db) -> DefKey { DefKey { file: self.file(db), + owner: self.owner(db).map(|owner| Box::new(owner.key(db))), kind: self.kind(db), name: self.name(db), + fingerprint: self.fingerprint(db), disambiguator: self.disambiguator(db), } } pub(crate) fn from_key(db: &'db dyn crate::Db, key: &DefKey) -> Self { - DefId::new(db, key.file, key.kind, key.name.clone(), key.disambiguator) + let owner = key.owner.as_deref().map(|owner| DefId::from_key(db, owner)); + DefId::new( + db, + key.file, + owner, + key.kind, + key.name.clone(), + key.fingerprint.clone(), + key.disambiguator, + ) } } +/// Current absolute base location for a definition anchor. +/// +/// This is produced by lowering and looked up only when anchor-relative spans +/// need to cross an output boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct DefLocation { + /// File that currently contains the definition base. pub file: SourceFile, + /// Absolute byte offset used as the base for def-relative spans. pub base_offset: Offset, } +/// One entry in a per-file definition location table. +/// +/// The precomputed hash is an index aid only; equality on `def_id` remains the +/// authority so hash collisions cannot resolve to the wrong definition. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct DefLocationEntry<'db> { + /// Stable hash of `def_id` used to binary-search the table. pub hash: u64, + /// Definition whose base location is recorded. pub def_id: DefId<'db>, + /// Current absolute location of the definition base. pub location: DefLocation, } +/// Sorted location table for def anchors in one parsed source file. +/// +/// The table is produced during lowering and injected back into HIR through the +/// database. It is intentionally consulted only when relative spans cross an +/// output boundary and need absolute offsets. #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, salsa::Update)] pub struct DefLocationTable<'db> { /// Entries sorted by `DefLocationEntry::hash` ascending. @@ -90,6 +179,13 @@ pub struct DefLocationTable<'db> { } impl<'db> DefLocationTable<'db> { + /// Builds a sorted location table from definition/location pairs. + /// + /// # Panics + /// + /// Panics if the same [`DefId`] appears more than once. Multiple distinct + /// definitions may share a hash; lookup verifies equality after narrowing + /// to the hash range. pub fn from_def_locations( entries: impl IntoIterator, DefLocation)>, ) -> Self { @@ -116,14 +212,11 @@ impl<'db> DefLocationTable<'db> { } } -#[salsa::tracked(returns(ref))] -pub fn def_locations_for_file<'db>( - _db: &'db dyn crate::Db, - _file: SourceFile, -) -> DefLocationTable<'db> { - todo!() -} - +/// Resolves `def` through a prebuilt location table. +/// +/// Returns `None` when the table does not contain the definition. Callers at +/// diagnostic or LSP edges usually treat that as an internal invariant break; +/// semantic queries should avoid calling this and keep spans relative. pub fn resolve_def_location<'db>( table: &DefLocationTable<'db>, def: DefId<'db>, @@ -142,7 +235,23 @@ pub fn resolve_def_location<'db>( .map(|entry| entry.location) } +/// Resolves `def` or panics with a compiler-bug invariant message. +/// +/// This helper is for output-edge span resolution only. Tracked semantic +/// queries should keep spans relative instead of reading def-location tables. +pub(crate) fn resolve_def_location_or_bug<'db>( + table: &DefLocationTable<'db>, + def: DefId<'db>, + context: &'static str, + debug_key: impl fmt::Debug, +) -> DefLocation { + resolve_def_location(table, def) + .unwrap_or_else(|| panic!("missing DefLocation for {}: {:?}", context, debug_key)) +} + fn def_id_hash<'db>(def: DefId<'db>) -> u64 { + // This table key intentionally uses std SipHash rather than FxHash so the + // persisted order does not depend on rustc_hash implementation details. let mut hasher = DefaultHasher::new(); def.hash(&mut hasher); hasher.finish() @@ -151,31 +260,48 @@ fn def_id_hash<'db>(def: DefId<'db>) -> u64 { #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct DefBaseKey { file: SourceFile, + owner: Option>, kind: DefKind, name: Option, + fingerprint: Option, } /// Stateful allocator for deterministic disambiguators during lowering/parsing. +/// +/// A fresh canonicalizer is used for one lowering pass. It remembers how many +/// times each base key has appeared and assigns duplicate ordinals in source +/// traversal order, while leaving unique definitions at +/// [`Disambiguator::ZERO`]. #[derive(Debug, Default)] pub struct KeyCanonicalizer { - def_counts: HashMap, + def_counts: FxHashMap, } impl KeyCanonicalizer { + /// Creates an empty canonicalizer for one lowering pass. pub fn new() -> Self { Self::default() } - pub fn next_def_disambiguator( + /// Allocates the next duplicate ordinal for a structural def base key. + /// + /// The `owner`, `kind`, `name`, and `fingerprint` form the duplicate class. + /// The returned value should be stored in the eventual [`DefId`]. + pub fn next_def_disambiguator<'db>( &mut self, + db: &'db dyn crate::Db, file: SourceFile, + owner: Option>, kind: DefKind, name: Option<&str>, + fingerprint: Option<&str>, ) -> Disambiguator { let base = DefBaseKey { file, + owner: owner.map(|owner| Box::new(owner.key(db))), kind, name: name.map(ToOwned::to_owned), + fingerprint: fingerprint.map(ToOwned::to_owned), }; let count = self.def_counts.entry(base).or_insert(0); let disambiguator = Disambiguator::new(*count); @@ -183,14 +309,29 @@ impl KeyCanonicalizer { disambiguator } + /// Interns a [`DefId`] with the next deterministic disambiguator. + /// + /// This is the normal construction path during lowering. Use + /// [`Self::next_def_disambiguator`] only when the caller needs to inspect + /// or store the ordinal separately. pub fn alloc_def<'db>( &mut self, db: &'db dyn crate::Db, file: SourceFile, + owner: Option>, kind: DefKind, name: Option<&str>, + fingerprint: Option<&str>, ) -> DefId<'db> { - let disambiguator = self.next_def_disambiguator(file, kind, name); - DefId::new(db, file, kind, name.map(ToOwned::to_owned), disambiguator) + let disambiguator = self.next_def_disambiguator(db, file, owner, kind, name, fingerprint); + DefId::new( + db, + file, + owner, + kind, + name.map(ToOwned::to_owned), + fingerprint.map(ToOwned::to_owned), + disambiguator, + ) } } diff --git a/crates/hir/src/arena.rs b/crates/hir/src/arena.rs index 06c5a869..6e7d9da6 100644 --- a/crates/hir/src/arena.rs +++ b/crates/hir/src/arena.rs @@ -1,20 +1,56 @@ +//! Typed index arena for HIR bodies. +//! +//! Function bodies store statements, expressions, and patterns in compact +//! arenas so recursive references can be represented by copyable IDs rather +//! than by nested boxes. An `Id` is meaningful only for the `Arena` that +//! allocated it. + use std::{ marker::PhantomData, ops::{Index, IndexMut}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +/// Typed index into an [`Arena`]. +/// +/// The `T` marker prevents accidentally indexing an expression arena with a +/// statement ID. IDs are stable for the lifetime of the arena because the arena +/// never removes or reorders items. +#[derive(Debug, PartialEq, Eq, Hash, salsa::Update)] pub struct Id { raw: u32, _marker: PhantomData T>, } +impl Clone for Id { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Id {} + impl Id { + fn from_usize(raw: usize) -> Self { + Self { + raw: u32::try_from(raw).expect("arena index exceeds u32::MAX"), + _marker: PhantomData, + } + } + + /// Returns the zero-based arena index for this ID. + /// + /// This is mainly for diagnostics, iteration, and implementing indexing. + /// It does not identify an item outside the arena that allocated it. pub fn as_usize(self) -> usize { self.raw as usize } } +/// Append-only typed arena. +/// +/// The arena gives HIR bodies stable intra-body IDs without interning every +/// expression or statement in Salsa. Items can be mutated before the body is +/// frozen into a tracked value; after that, callers normally use shared access. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update, Default)] pub struct Arena where @@ -27,45 +63,57 @@ impl Arena where T: salsa::Update, { + /// Creates an empty arena. pub fn new() -> Self { Self { items: Vec::new() } } + /// Appends `value` and returns its typed ID. + /// + /// # Panics + /// + /// Panics if the next zero-based index exceeds `u32::MAX`. The arena is not + /// modified when this check fails. pub fn alloc(&mut self, value: T) -> Id { - let id = Id { - raw: self.items.len() as u32, - _marker: PhantomData, - }; + let id = Id::from_usize(self.items.len()); self.items.push(value); id } + /// Returns the item for `id`. + /// + /// # Panics + /// + /// Panics if `id` was not allocated by this arena. pub fn get(&self, id: Id) -> &T { &self.items[id.as_usize()] } + /// Returns a mutable item for `id`. + /// + /// # Panics + /// + /// Panics if `id` was not allocated by this arena. pub fn get_mut(&mut self, id: Id) -> &mut T { &mut self.items[id.as_usize()] } + /// Returns the number of allocated items. pub fn len(&self) -> usize { self.items.len() } + /// Returns whether the arena contains no items. pub fn is_empty(&self) -> bool { self.items.is_empty() } + /// Iterates over allocated IDs and their items in allocation order. pub fn iter(&self) -> impl Iterator, &T)> { - self.items.iter().enumerate().map(|(i, v)| { - ( - Id { - raw: i as u32, - _marker: PhantomData, - }, - v, - ) - }) + self.items + .iter() + .enumerate() + .map(|(index, value)| (Id::from_usize(index), value)) } } @@ -88,3 +136,22 @@ where self.get_mut(index) } } + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn id_preserves_representable_usize_index() { + let id = Id::<()>::from_usize(42); + + assert_eq!(id.as_usize(), 42); + } + + #[cfg(target_pointer_width = "64")] + #[test] + #[should_panic(expected = "arena index exceeds u32::MAX")] + fn id_rejects_usize_index_larger_than_u32() { + let _ = Id::<()>::from_usize(usize::MAX); + } +} diff --git a/crates/hir/src/ast.rs b/crates/hir/src/ast.rs index 003a9a61..fb17f6c8 100644 --- a/crates/hir/src/ast.rs +++ b/crates/hir/src/ast.rs @@ -1,9 +1,34 @@ +//! Lowered abstract syntax tree nodes. +//! +//! The AST in this crate is already HIR: syntax has been parsed and normalized +//! into Salsa-backed definitions, body arenas, and anchor-relative spans. +//! Identifiers are interned once and then paired with spans through +//! [`crate::span::SpannedElem`] wherever source locations matter. + +/// Function signatures, bodies, statements, expressions, patterns, and Yul. pub mod function; +/// Top-level and contract-level item definitions. pub mod item; +/// Unresolved type and predicate references. pub mod ty; +pub use item::{SourceComment, SourceCommentKind, SourceComments}; + +/// Interned identifier text. +/// +/// `Ident` intentionally stores only the textual name. Source position and +/// syntactic role live outside it so identical names across the program share +/// one interned value while callers can still attach precise spans. #[salsa::interned(debug)] pub struct Ident<'db> { + /// Identifier text exactly as accepted by the parser/lowerer. #[returns(ref)] - name: String, + pub name: String, +} + +impl<'db> Ident<'db> { + /// Returns the identifier text interned in the database. + pub fn text(self, db: &'db dyn crate::Db) -> &'db str { + self.name(db) + } } diff --git a/crates/hir/src/ast/function.rs b/crates/hir/src/ast/function.rs index 49737096..aa63b9cd 100644 --- a/crates/hir/src/ast/function.rs +++ b/crates/hir/src/ast/function.rs @@ -1,3 +1,12 @@ +//! Function, statement, expression, pattern, and Yul HIR nodes. +//! +//! Function bodies are arena-backed: statements, expressions, and patterns +//! refer to each other by typed arena IDs. This avoids recursive ownership +//! cycles and keeps body-local references compact. The `Error` variants in this +//! file are recovery sentinels and should stay silent; parse diagnostics are +//! collected during parsing/lowering, and visitors can inspect these nodes +//! separately. + use crate::{ Db, anchor::DefId, @@ -9,14 +18,115 @@ use crate::{ span::{Span, Spanned, SpannedElem}, }; +/// Source-level visibility modifier on a function declaration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum FunctionVisibility { + /// Externally callable and internally reusable. + Public, + /// Externally callable through the ABI. + External, + /// Reusable within the declaring program/library. + Internal, + /// Visible only within the declaring contract-like scope. + Private, +} + +impl FunctionVisibility { + /// Returns the canonical source keyword. + pub const fn keyword(self) -> &'static str { + match self { + Self::Public => "public", + Self::External => "external", + Self::Internal => "internal", + Self::Private => "private", + } + } + + /// Returns whether the declaration contributes to an external ABI. + pub const fn is_abi_visible(self) -> bool { + matches!(self, Self::Public | Self::External) + } +} + +/// Explicit source-level state-mutability modifier. +/// +/// The absence of a modifier denotes Solidity's `nonpayable` ABI state. +/// This node records the declaration; effect validation is a separate semantic +/// concern rather than a property guaranteed by lowering. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum FunctionMutability { + /// Source `pure` annotation. + Pure, + /// Source `view` annotation. + View, + /// Source `payable` annotation. + Payable, +} + +impl FunctionMutability { + /// Returns the canonical source keyword. + pub const fn keyword(self) -> &'static str { + match self { + Self::Pure => "pure", + Self::View => "view", + Self::Payable => "payable", + } + } +} + +/// Lowered function signature shared by functions, methods, lambdas, and ABI +/// forms. +/// +/// The signature stores source-level types and predicates, not checked types. +/// Explicit modifiers keep their keyword spans so diagnostics can point at +/// modifier misuse without inventing implicit source syntax. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct FuncSig<'db> { + /// Span covering the complete signature syntax. pub span: Span<'db>, + /// Explicit type parameters introduced by the function's `<...>` list. pub type_vars: Vec>>, + /// Trait constraints that qualify this signature. pub preds: Vec>, + /// Explicit visibility keyword and its source span. + pub visibility: Option>, + /// Explicit state-mutability keyword and its source span. + pub mutability: Option>, + /// Function or method name. pub name: SpannedElem<'db, Ident<'db>>, + /// Parameters and the span of the parameter list. pub params: SpannedElem<'db, Vec>>, + /// Optional explicit return type. pub ret: Option>, + /// Optional source names for each top-level return value. + /// + /// This vector is parallel to the entries in the source `returns (...)` + /// list. `None` preserves an unnamed entry without conflating it with an + /// omitted or empty return list. + pub ret_names: Vec>>>, +} + +impl FuncSig<'_> { + /// Returns the explicit visibility kind, if one was written. + pub fn visibility_kind(&self) -> Option { + self.visibility.map(|visibility| *visibility.atom()) + } + + /// Returns the explicit state-mutability kind, if one was written. + pub fn mutability_kind(&self) -> Option { + self.mutability.map(|mutability| *mutability.atom()) + } + + /// Returns whether the declaration contributes to an external ABI. + pub fn is_abi_visible(&self) -> bool { + self.visibility_kind() + .is_some_and(FunctionVisibility::is_abi_visible) + } + + /// Returns whether the declaration may receive value. + pub fn is_payable(&self) -> bool { + self.mutability_kind() == Some(FunctionMutability::Payable) + } } impl<'db> Spanned<'db> for FuncSig<'db> { @@ -25,258 +135,541 @@ impl<'db> Spanned<'db> for FuncSig<'db> { } } +/// Lowered function body with arena-owned statements, expressions, and +/// patterns. +/// +/// The body is a definition so spans inside it can be relative to the body base +/// rather than to the whole file. `top_level_stmts` preserves execution order; +/// the arenas may also contain nested nodes referenced from those statements. #[salsa::tracked(debug)] pub struct FuncBody<'db> { + /// Structural identity of this body. #[tracked] #[returns(copy)] pub def_id: DefId<'db>, + /// Span covering the body braces and contents, relative to the body anchor. #[tracked] #[returns(copy)] pub span: Span<'db>, + /// Statement IDs that form the body's top-level sequence. #[tracked] #[returns(ref)] pub top_level_stmts: Vec>>, + /// Arena containing all statements in this body. #[tracked] #[returns(ref)] pub stmts: Arena>, + /// Arena containing all expressions in this body. #[tracked] #[returns(ref)] pub exprs: Arena>, + /// Arena containing all patterns in this body. #[tracked] #[returns(ref)] pub pats: Arena>, } +/// Statement node stored in a function-body arena. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct Stmt<'db> { + /// Span covering the statement syntax. pub span: Span<'db>, + /// Statement payload. pub kind: StmtKind<'db>, } +/// Assignment operator used by a statement. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum AssignOp { + /// `=` assignment. + Plain, + /// `+=` assignment. + Add, + /// `-=` assignment. + Sub, + /// `^=` assignment. + BitXor, + /// `&=` assignment. + BitAnd, + /// `|=` assignment. + BitOr, + /// `%=` assignment. + Mod, +} + +/// Kinds of statements accepted in lowered function bodies. +/// +/// Child expressions, patterns, and statements are referenced by IDs into the +/// owning [`FuncBody`] arenas. The resolver relies on this shape for lexical +/// scoping; for example `let` initializers are resolved before their binders +/// are inserted. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum StmtKind<'db> { + /// Local binding statement. Let { + /// Span of an optional `comptime` marker. + comptime: Option>, + /// Binder name. name: SpannedElem<'db, Ident<'db>>, + /// Optional type annotation. ty: Option>, + /// Optional initializer expression. init: Option>>, }, + /// Return from the current function, optionally with a value. Return(Option>>), + /// Expression used as a statement. Expr(Id>), + /// Assignment. Assign { + /// Assignment operator. + op: AssignOp, + /// Assignment target expression. lhs: Id>, + /// Assigned value expression. rhs: Id>, }, - AddAssign { - lhs: Id>, - rhs: Id>, - }, - SubAssign { - lhs: Id>, - rhs: Id>, - }, + /// Pattern-matching statement. Match { + /// Scrutinee expressions matched by each arm. scrutinees: Vec>>, + /// Match arms in source order. arms: Vec>, }, + /// C-style `for` loop. + For { + /// Initializer statements. + init: Vec>>, + /// Loop condition expression. + cond: Id>, + /// Post-iteration statements. + post: Vec>>, + /// Loop body statements. + body: Vec>>, + }, + /// Conditional statement. If { + /// Condition expression. cond: Id>, + /// Statements executed when the condition is true. then_body: Vec>>, + /// Optional `else` body. else_body: Option>>>, }, + /// Lexical block. + Block { + /// Statements inside the block. + body: Vec>>, + }, + /// Inline Yul assembly block. Assembly { + /// Lowered Yul statements. body: Vec>, }, + /// Loop break. + Break, + /// Loop continue. + Continue, + /// Parser recovery placeholder. Error, } +/// Expression node stored in a function-body arena. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct Expr<'db> { + /// Span covering the expression syntax. pub span: Span<'db>, + /// Expression payload. pub kind: ExprKind<'db>, } +/// Kinds of expressions accepted in lowered function bodies. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum ExprKind<'db> { + /// Literal expression. Lit(LitKind), + /// Identifier expression before name resolution. Ident(SpannedElem<'db, Ident<'db>>), + /// Leading-dot constructor expression such as `.Ctor(...)`. + DotCtor { + /// Span of the leading dot. + dot: Span<'db>, + /// Constructor leaf name. + name: SpannedElem<'db, Ident<'db>>, + /// Constructor arguments. + args: Vec>>, + }, + /// Type proxy expression introduced by `@`. + Proxy { + /// Span of the `@` token. + at: Span<'db>, + /// Proxied type reference. + ty: TypeRef<'db>, + }, + /// Lambda expression with a separately lowered body. Lambda { + /// Lambda parameters and parameter-list span. params: SpannedElem<'db, Vec>>, + /// Optional return type annotation. ret: Option>, + /// Body owned by the lambda. body: FuncBody<'db>, }, + /// Binary operator expression. BinOp { + /// Left operand. lhs: Id>, + /// Operator and its token span. op: SpannedElem<'db, BinOp>, + /// Right operand. rhs: Id>, }, + /// Indexing expression. Index { + /// Indexed expression. base: Id>, + /// Index expression. index: Id>, }, + /// Function or constructor call. Call { + /// Callee expression. callee: Id>, + /// Argument expressions. args: Vec>>, }, + /// Field or namespace selection. Field { + /// Base expression. base: Id>, + /// Selected field or path segment. field: SpannedElem<'db, Ident<'db>>, }, - TypeAnnot { + /// Explicit `expression as Type` conversion. + Conversion { + /// Converted expression. expr: Id>, + /// Conversion target type. ty: TypeRef<'db>, }, + /// Internal type ascription introduced by lowering/generated HIR. + /// + /// This is intentionally distinct from a source-level conversion: an + /// ascription guides inference and is erased before backend lowering. + TypeAscription { + /// Ascribed expression. + expr: Id>, + /// Expected type for the expression. + ty: TypeRef<'db>, + }, + /// Unary operator expression. UnaryOp { + /// Operator and token span. op: SpannedElem<'db, UnOp>, + /// Operand expression. expr: Id>, }, + /// Conditional expression. If { + /// Condition expression. cond: Id>, + /// Value when the condition is true. then_expr: Id>, + /// Value when the condition is false. else_expr: Id>, }, + /// Tuple expression; an empty tuple is the unit value. + Tuple(Vec>>), + /// Parser recovery placeholder. Error, } +/// One arm of a match statement. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct MatchArm<'db> { + /// Span covering the arm, including its leading separator. pub span: Span<'db>, + /// Patterns matched against the statement scrutinees. pub pats: Vec>>, + /// Body statements for this arm. pub body: Vec>>, } +/// Pattern node stored in a function-body arena. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct Pat<'db> { + /// Span covering the pattern syntax. pub span: Span<'db>, + /// Pattern payload. pub kind: PatKind<'db>, } +/// Constructor pattern head syntax. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum PatCtorHead<'db> { + /// Leading-dot constructor lookup deferred to the expected type. + Deferred { + /// Span of the leading dot. + dot: Span<'db>, + /// Constructor leaf name. + name: SpannedElem<'db, Ident<'db>>, + }, + /// Qualified constructor lookup. + Qualified { + /// Qualifier path collapsed into a dotted identifier. + qualifier: SpannedElem<'db, Ident<'db>>, + /// Constructor leaf name. + name: SpannedElem<'db, Ident<'db>>, + }, + /// Unqualified constructor or variable-like pattern head. + Unqualified { + /// Constructor leaf name. + name: SpannedElem<'db, Ident<'db>>, + }, +} + +impl<'db> PatCtorHead<'db> { + pub fn name(&self) -> &SpannedElem<'db, Ident<'db>> { + match self { + Self::Deferred { name, .. } + | Self::Qualified { name, .. } + | Self::Unqualified { name } => name, + } + } +} + +/// Kinds of patterns accepted by match arms. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum PatKind<'db> { + /// `_` wildcard pattern. Wildcard, + /// Variable binding pattern. Var(SpannedElem<'db, Ident<'db>>), + /// Literal pattern. Lit(LitKind), + /// Constructor pattern, possibly qualified. Ctor { - name: SpannedElem<'db, Ident<'db>>, + /// Constructor pattern head syntax. + head: PatCtorHead<'db>, + /// Constructor argument patterns. args: Vec>>, }, + /// `comptime` pattern label. + ComptimeLabel { + /// Span of the `comptime` keyword. + kw: Span<'db>, + /// Expression attached to the label. + expr: Id>, + }, + /// Tuple pattern. Tuple { + /// Element patterns. elems: Vec>>, }, + /// Parser recovery placeholder. Error, } +/// Source literal kind shared by expressions and patterns. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum LitKind { + /// Decimal number literal text. Number(String), + /// Hexadecimal literal text. Hex(String), + /// Quoted string literal text. String(String), + /// Parser recovery placeholder for a malformed literal position. Error, } +/// Binary operators represented in HIR. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum BinOp { + /// Addition. Add, + /// Subtraction. Sub, + /// Multiplication. Mul, + /// Division. Div, + /// Remainder. Mod, + /// Exponentiation. + Pow, + /// Left shift. + Shl, + /// Logical right shift. + Shr, + /// Bitwise and. + BitAnd, + /// Bitwise xor. + BitXor, + /// Bitwise or. + BitOr, + /// Equality. Eq, + /// Inequality. NotEq, + /// Less-than comparison. Lt, + /// Greater-than comparison. Gt, + /// Less-than-or-equal comparison. LtEq, + /// Greater-than-or-equal comparison. GtEq, + /// Logical and. And, + /// Logical or. Or, + /// Parser recovery placeholder. Error, } +/// Unary operators represented in HIR. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum UnOp { + /// Logical negation. Not, + /// Parser recovery placeholder. Error, } +/// Inline Yul statement node. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct YulStmt<'db> { + /// Span covering the Yul statement. pub span: Span<'db>, + /// Yul statement payload. pub kind: YulStmtKind<'db>, } +/// Kinds of inline Yul statements. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum YulStmtKind<'db> { + /// Braced statement block. Block(Vec>), + /// Yul `let` binding. Let { + /// Bound names. names: Vec>>, + /// Optional initializer. init: Option>, }, + /// Yul assignment. Assign { + /// Assigned names. names: Vec>>, + /// Assigned value. value: YulExpr<'db>, }, + /// Expression statement. Expr(YulExpr<'db>), + /// Yul conditional. If { + /// Condition expression. cond: YulExpr<'db>, + /// Body statements. body: Vec>, }, + /// Yul `for` loop. For { + /// Initializer statements. init: Vec>, + /// Condition expression. cond: YulExpr<'db>, + /// Post-iteration statements. post: Vec>, + /// Body statements. body: Vec>, }, + /// Yul `switch` statement. Switch { + /// Scrutinee expression. expr: YulExpr<'db>, + /// Explicit cases. cases: Vec>, + /// Optional default body. default: Option>>, }, + /// Inline Yul function definition. FunctionDef { + /// Function name. name: SpannedElem<'db, Ident<'db>>, + /// Parameter names. params: Vec>>, + /// Return names. rets: Vec>>, + /// Function body. body: Vec>, }, + /// Yul `leave`. Leave, + /// Yul `break`. Break, + /// Yul `continue`. Continue, + /// Parser recovery placeholder. Error, } +/// Inline Yul expression node. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct YulExpr<'db> { + /// Span covering the Yul expression. pub span: Span<'db>, + /// Yul expression payload. pub kind: YulExprKind<'db>, } +/// Kinds of inline Yul expressions. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum YulExprKind<'db> { + /// Literal expression. Lit(YulLitKind), + /// Identifier expression. Ident(SpannedElem<'db, Ident<'db>>), + /// Function call expression. Call { + /// Callee name. name: SpannedElem<'db, Ident<'db>>, + /// Argument expressions. args: Vec>, }, + /// Parser recovery placeholder. Error, } +/// Inline Yul literal kind. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum YulLitKind { + /// Decimal number literal text. Number(String), + /// Hexadecimal literal text. Hex(String), + /// Quoted string literal text. String(String), + /// Boolean literal. Bool(bool), + /// Parser recovery placeholder. Error, } +/// One case in a Yul switch. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct YulCase<'db> { + /// Span covering the case label and body. pub span: Span<'db>, + /// Literal matched by the case. pub lit: YulLitKind, + /// Statements executed for this case. pub body: Vec>, } @@ -328,26 +721,48 @@ impl<'db> Spanned<'db> for YulCase<'db> { } } +/// Function or lambda parameter syntax. +/// +/// Parameters can be typed or untyped at this stage because different syntactic +/// contexts allow different requirements. Semantic phases decide whether a +/// particular untyped parameter is legal. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum FuncParam<'db> { + /// Parameter with an explicit type. Typed { + /// Span of an optional `comptime` marker. + comptime: Option>, + /// Parameter name. name: SpannedElem<'db, Ident<'db>>, + /// Parameter type annotation. ty: TypeRef<'db>, }, + /// Parameter without a type annotation. Untyped { + /// Span of an optional `comptime` marker. + comptime: Option>, + /// Parameter name. name: SpannedElem<'db, Ident<'db>>, }, - Error, + /// Parser recovery placeholder for a malformed parameter. + Error { + /// Span covering the unparseable parameter syntax. + span: Span<'db>, + }, } impl<'db> Spanned<'db> for FuncParam<'db> { fn span(&self, db: &'db dyn Db) -> Span<'db> { match self { - Self::Typed { name, ty } => name.span(db) + ty.span(db), - Self::Untyped { name } => name.span(db), - Self::Error => panic!("FuncParam::Error has no span"), + Self::Typed { comptime, name, ty } => { + comptime.map_or_else(|| name.span(db), |kw| kw + name.span(db)) + ty.span(db) + } + Self::Untyped { comptime, name } => { + comptime.map_or_else(|| name.span(db), |kw| kw + name.span(db)) + } + Self::Error { span } => *span, } } } diff --git a/crates/hir/src/ast/item.rs b/crates/hir/src/ast/item.rs index a617636f..5c5df5ef 100644 --- a/crates/hir/src/ast/item.rs +++ b/crates/hir/src/ast/item.rs @@ -1,35 +1,66 @@ +//! Top-level and contract-level item HIR. +//! +//! Items are the named declarations that participate in structural identity, +//! module interfaces, and name resolution. Most item definitions are Salsa +//! tracked structs keyed by a [`crate::anchor::DefId`] so later phases can +//! refer to stable identities while still reading fields incrementally. + use crate::{ Db, anchor::DefId, + arena::{Arena, Id}, ast::{ Ident, - function::{FuncBody, FuncSig}, + function::{Expr, FuncBody, FuncSig}, ty::{PredRef, TypeRef}, }, span::{Span, Spanned, SpannedElem}, }; +/// Algebraic data type declaration. +/// +/// The definition introduces a type name and a set of constructors. Constructor +/// terms are resolved through the owning data type rather than as bare global +/// values. #[salsa::tracked(debug)] pub struct AdtDef<'db> { + /// Stable structural identity of the data type. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] - name: SpannedElem<'db, Ident<'db>>, + #[returns(ref)] + pub leading_comments: Vec, + /// Declared type name. + #[tracked] + pub name: SpannedElem<'db, Ident<'db>>, + + /// Type parameters in source order. #[tracked] #[returns(ref)] - ty_params: Vec>>, + pub ty_params: Vec>>, /// Data constructors declared for this ADT. #[tracked] #[returns(ref)] - ctors: Vec>, + pub ctors: Vec>, + + /// Leading comments for each constructor, parallel to [`Self::ctors`]. + /// + /// The lowerer maintains the invariant that both vectors have equal + /// lengths. Keeping the payload in a separate tracked field distinguishes + /// documentation data from semantic constructor data. + #[tracked] + #[returns(ref)] + pub ctor_comments: Vec>, } impl<'db> Spanned<'db> for AdtDef<'db> { @@ -38,15 +69,88 @@ impl<'db> Spanned<'db> for AdtDef<'db> { } } +impl<'db> AdtDef<'db> { + fn assert_comment_alignment(&self, db: &'db dyn Db) { + assert_eq!( + self.ctors(db).len(), + self.ctor_comments(db).len(), + "ADT constructors and constructor comments must remain aligned" + ); + } + + /// Returns the stable definition identity for this ADT. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + AdtDef::def_id(*self, db) + } + + /// Returns the ADT name with its declaration span. + pub fn name_elem(&self, db: &'db dyn Db) -> SpannedElem<'db, Ident<'db>> { + AdtDef::name(*self, db) + } + + /// Returns type parameters with their binder spans. + pub fn ty_param_elems(&self, db: &'db dyn Db) -> &Vec>> { + AdtDef::ty_params(*self, db) + } + + /// Returns leading comments for the constructor at `index`. + pub fn ctor_leading_comments( + &self, + db: &'db dyn Db, + index: usize, + ) -> Option<&'db [SourceComment]> { + self.assert_comment_alignment(db); + self.ctor_comments(db).get(index).map(Vec::as_slice) + } + + /// Iterates over constructors paired with their leading comments. + pub fn ctors_with_comments( + &self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator, &'db [SourceComment])> + 'db { + self.assert_comment_alignment(db); + self.ctors(db) + .iter() + .zip(self.ctor_comments(db).iter().map(Vec::as_slice)) + } +} + +/// Constructor declared by an algebraic data type. +/// +/// Constructor fields are represented as a single tuple-like type reference so +/// nullary, unary, and n-ary constructors share one representation. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct AdtCtor<'db> { + /// Constructor name. pub name: SpannedElem<'db, Ident<'db>>, + /// Constructor field type list and span. pub fields: SpannedElem<'db, TypeRef<'db>>, + /// Struct field names in source order, parallel to the constructor fields. + /// + /// Positional enum constructor payloads use `None`. + pub field_names: Option>>>, + /// Number of fields in the source constructor parameter list. + /// + /// This is kept separately because the lowered type reference intentionally + /// erases the outer tuple around a unary field. For example, `Wrap((a, b))` + /// and `Pair(a, b)` otherwise have the same lowered field type shape. + pub field_count: usize, } impl<'db> AdtCtor<'db> { - pub fn new(name: SpannedElem<'db, Ident<'db>>, fields: SpannedElem<'db, TypeRef<'db>>) -> Self { - Self { name, fields } + /// Creates an ADT constructor value. + pub fn new( + name: SpannedElem<'db, Ident<'db>>, + fields: SpannedElem<'db, TypeRef<'db>>, + field_names: Option>>>, + field_count: usize, + ) -> Self { + Self { + name, + fields, + field_names, + field_count, + } } } @@ -56,24 +160,112 @@ impl<'db> Spanned<'db> for AdtCtor<'db> { } } -/// Function definition. +/// Kind of callable declaration represented by [`FunctionDef`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum FuncKind { + /// Ordinary function or method declared with `function`. + Function, + /// Contract constructor. + Constructor, + /// Contract fallback function. + Fallback, +} + +/// Solidity-style declaration shell used for a contract-like item. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum ContractKind { + /// Deployable contract declaration. + Contract, + /// Interface declaration containing an external surface. + Interface, + /// Library declaration containing reusable definitions. + Library, +} + +impl ContractKind { + /// Returns the canonical source keyword for this declaration kind. + pub const fn keyword(self) -> &'static str { + match self { + Self::Contract => "contract", + Self::Interface => "interface", + Self::Library => "library", + } + } +} + +/// Lexical form of a source comment attached to a declaration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum SourceCommentKind { + /// A `// ...` comment. + Line, + /// A `/* ... */` comment. + Block, +} + +/// A source comment immediately preceding a declaration. +/// +/// `text` contains the comment body exactly as written between the delimiters; +/// it is deliberately not trimmed. Locations are not stored here because a +/// leading comment begins before the definition anchor. Keeping comment +/// payloads in their own tracked fields lets consumers depend on documentation +/// independently from the corresponding semantic item data when its spans are +/// otherwise unchanged. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct SourceComment { + /// Whether the source used line or block comment syntax. + pub kind: SourceCommentKind, + /// Comment contents without the outer `//` or `/* ... */` delimiters. + pub text: String, +} + +/// Interned source comments used by recovery items that have no tracked node. +/// +/// Ordinary declarations store their comments in a tracked field. Recovery +/// placeholders live directly in [`Item`] and [`ContractItem`], whose handles +/// are intentionally `Copy`; interning keeps those enum values lightweight +/// while retaining comments for tooling that also inspects malformed input. +#[salsa::interned(debug)] +pub struct SourceComments<'db> { + /// Comments in source order. + #[returns(ref)] + pub comments: Vec, +} + +/// Function, method, constructor, or fallback definition. +/// +/// The signature is always present; the body is optional to allow signatures in +/// contexts that do not contain executable code. #[salsa::tracked(debug)] pub struct FunctionDef<'db> { + /// Stable structural identity of the function. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the complete definition or declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Callable category. + #[tracked] + #[returns(copy)] + pub kind: FuncKind, + + /// Consecutive source comments directly leading this callable. #[tracked] #[returns(ref)] - sig: FuncSig<'db>, + pub leading_comments: Vec, + /// Source-level signature. + #[tracked] + #[returns(ref)] + pub sig: FuncSig<'db>, + + /// Optional lowered body. #[tracked] #[returns(copy)] - body: Option>, + pub body: Option>, } impl<'db> Spanned<'db> for FunctionDef<'db> { @@ -82,28 +274,58 @@ impl<'db> Spanned<'db> for FunctionDef<'db> { } } -/// Type alias definition: `type Name(T, U) = Type`. +impl<'db> FunctionDef<'db> { + /// Returns the stable definition identity for this function. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + FunctionDef::def_id(*self, db) + } +} + +/// Semantic family of a source type declaration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum TypeAliasKind { + /// `alias Name = Type` is a transparent type synonym. + Transparent, + /// `type Name is Type` is a nominal user-defined value type. + ValueType, +} + +/// Type alias or value-type definition: `alias Name = Type` or +/// `type Name is Type`. #[salsa::tracked(debug)] pub struct TypeAlias<'db> { + /// Stable structural identity of the alias. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full alias declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] - name: SpannedElem<'db, Ident<'db>>, + #[returns(ref)] + pub leading_comments: Vec, + + /// Semantic family selected by the declaration keyword. + #[tracked] + #[returns(copy)] + pub kind: TypeAliasKind, + + /// Alias name. + #[tracked] + pub name: SpannedElem<'db, Ident<'db>>, /// Type parameters declared by this alias. #[tracked] #[returns(ref)] - ty_params: Vec>>, + pub ty_params: Vec>>, /// Aliased type. #[tracked] - ty: TypeRef<'db>, + pub ty: TypeRef<'db>, } impl<'db> Spanned<'db> for TypeAlias<'db> { @@ -112,31 +334,71 @@ impl<'db> Spanned<'db> for TypeAlias<'db> { } } +impl<'db> TypeAlias<'db> { + /// Returns the stable definition identity for this alias. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + TypeAlias::def_id(*self, db) + } + + /// Returns the alias name with its declaration span. + pub fn name_elem(&self, db: &'db dyn Db) -> SpannedElem<'db, Ident<'db>> { + TypeAlias::name(*self, db) + } + + /// Returns type parameters with their binder spans. + pub fn ty_param_elems(&self, db: &'db dyn Db) -> &Vec>> { + TypeAlias::ty_params(*self, db) + } +} + /// Type class definition. +/// +/// Classes introduce a type-namespace name and method names qualified by the +/// class during name resolution. #[salsa::tracked(debug)] pub struct ClassDef<'db> { + /// Stable structural identity of the class. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full class declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] #[returns(ref)] - type_vars: Vec>>, + pub leading_comments: Vec, + /// Type variables introduced by the class head. #[tracked] #[returns(ref)] - super_preds: Vec>, + pub type_vars: Vec>>, + /// Superclass predicates. #[tracked] - head: PredRef<'db>, + #[returns(ref)] + pub super_preds: Vec>, + /// Class head predicate naming the class. + #[tracked] + pub head: PredRef<'db>, + + /// Method signatures declared by the class. #[tracked] #[returns(ref)] - methods: Vec>, + pub methods: Vec>, + + /// Leading comments for each method, parallel to [`Self::methods`]. + /// + /// The lowerer maintains the invariant that both vectors have equal + /// lengths. Keeping the payload in a separate tracked field distinguishes + /// documentation data from semantic method signatures. + #[tracked] + #[returns(ref)] + pub method_comments: Vec>, } impl<'db> Spanned<'db> for ClassDef<'db> { @@ -145,34 +407,91 @@ impl<'db> Spanned<'db> for ClassDef<'db> { } } +impl<'db> ClassDef<'db> { + fn assert_comment_alignment(&self, db: &'db dyn Db) { + assert_eq!( + self.methods(db).len(), + self.method_comments(db).len(), + "trait methods and method comments must remain aligned" + ); + } + + /// Returns the stable definition identity for this class. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + ClassDef::def_id(*self, db) + } + + /// Returns type variables with their binder spans. + pub fn type_var_elems(&self, db: &'db dyn Db) -> &Vec>> { + ClassDef::type_vars(*self, db) + } + + /// Returns leading comments for the method at `index`. + pub fn method_leading_comments( + &self, + db: &'db dyn Db, + index: usize, + ) -> Option<&'db [SourceComment]> { + self.assert_comment_alignment(db); + self.method_comments(db).get(index).map(Vec::as_slice) + } + + /// Iterates over method signatures paired with their leading comments. + pub fn methods_with_comments( + &self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator, &'db [SourceComment])> + 'db { + self.assert_comment_alignment(db); + self.methods(db) + .iter() + .zip(self.method_comments(db).iter().map(Vec::as_slice)) + } +} + +/// Type class instance definition. +/// +/// Instance identity may use a structural fingerprint of its head so multiple +/// instances for the same class can remain distinct without relying on spans. #[salsa::tracked(debug)] pub struct InstanceDef<'db> { + /// Stable structural identity of the instance. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full instance declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] #[returns(ref)] - type_vars: Vec>>, + pub leading_comments: Vec, + /// Instance type variables. #[tracked] #[returns(ref)] - preds: Vec>, + pub type_vars: Vec>>, + /// Context predicates required by the instance. + #[tracked] + #[returns(ref)] + pub preds: Vec>, + + /// Span of the optional `default` keyword. #[tracked] #[returns(copy)] - default_kw: Option>, + pub default_kw: Option>, + /// Instance head predicate. #[tracked] - head: PredRef<'db>, + pub head: PredRef<'db>, + /// Method implementations declared in the instance body. #[tracked] #[returns(ref)] - methods: Vec>, + pub methods: Vec>, } impl<'db> Spanned<'db> for InstanceDef<'db> { @@ -181,39 +500,115 @@ impl<'db> Spanned<'db> for InstanceDef<'db> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +impl<'db> InstanceDef<'db> { + /// Returns the stable definition identity for this instance. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + InstanceDef::def_id(*self, db) + } + + /// Returns type variables with their binder spans. + pub fn type_var_elems(&self, db: &'db dyn Db) -> &Vec>> { + InstanceDef::type_vars(*self, db) + } +} + +/// Contract field declaration. +/// +/// Fields are private to their containing contract scope and are represented by +/// declaration order during name resolution. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct FieldInit<'db> { + /// Span covering the initializer expression. + pub span: Span<'db>, + /// Root expression ID in `exprs`. + pub root: Id>, + /// Arena containing the initializer expression tree. + pub exprs: Arena>, +} + +impl<'db> FieldInit<'db> { + /// Creates a contract field initializer. + pub fn new(span: Span<'db>, root: Id>, exprs: Arena>) -> Self { + Self { span, root, exprs } + } +} + +impl<'db> Spanned<'db> for FieldInit<'db> { + fn span(&self, _db: &'db dyn Db) -> Span<'db> { + self.span + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct FieldDef<'db> { name: SpannedElem<'db, Ident<'db>>, ty: TypeRef<'db>, + init: Option>, } impl<'db> FieldDef<'db> { - pub fn new(name: SpannedElem<'db, Ident<'db>>, ty: TypeRef<'db>) -> Self { - Self { name, ty } + /// Creates a contract field declaration. + pub fn new( + name: SpannedElem<'db, Ident<'db>>, + ty: TypeRef<'db>, + init: Option>, + ) -> Self { + Self { name, ty, init } } + /// Returns the field name with its binder span. pub fn name(&self) -> &SpannedElem<'db, Ident<'db>> { &self.name } + /// Returns the unresolved type annotation for the field. pub fn ty(&self) -> TypeRef<'db> { self.ty } + + /// Returns the optional field initializer expression. + pub fn init(&self) -> Option<&FieldInit<'db>> { + self.init.as_ref() + } } impl<'db> Spanned<'db> for FieldDef<'db> { fn span(&self, db: &'db dyn Db) -> Span<'db> { - self.name.span(db) + self.ty.span(db) + let span = self.name.span(db) + self.ty.span(db); + self.init.as_ref().map_or(span, |init| span + init.span(db)) } } /// Items that can appear inside a contract body. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum ContractItem<'db> { + /// Contract-local function, constructor, fallback, or method-like item. FunctionDef(FunctionDef<'db>), + /// Contract-local type alias. TypeAlias(TypeAlias<'db>), + /// Contract-local data type. AdtDef(AdtDef<'db>), - Error, + /// Parser recovery placeholder. + Error { + /// Span covering the recovered invalid contract item. + span: Span<'db>, + /// Consecutive source comments directly leading the invalid item. + leading_comments: SourceComments<'db>, + }, +} + +impl<'db> ContractItem<'db> { + /// Returns consecutive source comments directly leading this item. + pub fn leading_comments(self, db: &'db dyn Db) -> &'db [SourceComment] { + match self { + Self::FunctionDef(def) => def.leading_comments(db), + Self::TypeAlias(def) => def.leading_comments(db), + Self::AdtDef(def) => def.leading_comments(db), + Self::Error { + leading_comments, .. + } => leading_comments.comments(db), + } + } } impl<'db> Spanned<'db> for ContractItem<'db> { @@ -222,35 +617,65 @@ impl<'db> Spanned<'db> for ContractItem<'db> { Self::FunctionDef(def) => def.span(db), Self::TypeAlias(def) => def.span(db), Self::AdtDef(def) => def.span(db), - Self::Error => panic!("ContractItem::Error has no span"), + Self::Error { span, .. } => *span, } } } +/// Contract declaration. +/// +/// Contracts introduce a type name, fields, and a nested item scope. Name +/// resolution gives fields precedence over same-name functions when resolving +/// terms inside the contract body. #[salsa::tracked(debug)] pub struct ContractDef<'db> { + /// Stable structural identity of the contract. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full contract declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] - name: SpannedElem<'db, Ident<'db>>, + #[returns(ref)] + pub leading_comments: Vec, + /// Solidity-style declaration shell. + #[tracked] + #[returns(copy)] + pub kind: ContractKind, + + /// Contract name. + #[tracked] + pub name: SpannedElem<'db, Ident<'db>>, + + /// Contract type parameters in source order. #[tracked] #[returns(ref)] - ty_params: Vec>>, + pub ty_params: Vec>>, + /// Field declarations in source order. #[tracked] #[returns(ref)] - fields: Vec>, + pub fields: Vec>, + /// Leading comments for each field, parallel to [`Self::fields`]. + /// + /// The lowerer maintains the invariant that both vectors have equal + /// lengths. Keeping the payload in a separate tracked field distinguishes + /// documentation data from semantic field data. #[tracked] #[returns(ref)] - items: Vec>, + pub field_comments: Vec>, + + /// Nested contract items in source order. + #[tracked] + #[returns(ref)] + pub items: Vec>, } impl<'db> Spanned<'db> for ContractDef<'db> { @@ -259,19 +684,164 @@ impl<'db> Spanned<'db> for ContractDef<'db> { } } +impl<'db> ContractDef<'db> { + fn assert_comment_alignment(&self, db: &'db dyn Db) { + assert_eq!( + self.fields(db).len(), + self.field_comments(db).len(), + "contract fields and field comments must remain aligned" + ); + } + + /// Returns the stable definition identity for this contract. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + ContractDef::def_id(*self, db) + } + + /// Returns the contract name with its declaration span. + pub fn name_elem(&self, db: &'db dyn Db) -> SpannedElem<'db, Ident<'db>> { + ContractDef::name(*self, db) + } + + /// Returns type parameters with their binder spans. + pub fn ty_param_elems(&self, db: &'db dyn Db) -> &Vec>> { + ContractDef::ty_params(*self, db) + } + + /// Returns leading comments for the field at `index`. + pub fn field_leading_comments( + &self, + db: &'db dyn Db, + index: usize, + ) -> Option<&'db [SourceComment]> { + self.assert_comment_alignment(db); + self.field_comments(db).get(index).map(Vec::as_slice) + } + + /// Iterates over fields paired with their leading comments. + pub fn fields_with_comments( + &self, + db: &'db dyn Db, + ) -> impl ExactSizeIterator, &'db [SourceComment])> + 'db { + self.assert_comment_alignment(db); + self.fields(db) + .iter() + .zip(self.field_comments(db).iter().map(Vec::as_slice)) + } + + /// Returns whether this contract supplies its own ordinary runtime entry. + /// + /// This source-only predicate controls whether the compiler overlay adds a + /// generated dispatch entry. + pub fn has_runtime_main(&self, db: &'db dyn Db) -> bool { + self.items(db).iter().any(|item| { + matches!( + item, + ContractItem::FunctionDef(function) + if function.kind(db) == FuncKind::Function + && function.sig(db).name.atom().text(db) == "main" + ) + }) + } +} + +impl<'db> Import<'db> { + /// Returns import path segments with their source spans. + pub fn path_elems(&self, db: &'db dyn Db) -> &Vec>> { + Import::path(*self, db) + } + + /// Returns the optional import alias with its binder span. + pub fn alias_elem(&self, db: &'db dyn Db) -> Option>> { + Import::alias(*self, db) + } +} + +/// Constructor selector used by imports and exports. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ConstructorSelector<'db> { + /// Select every constructor of the named data type. + All, + /// Select only the named constructors. + Named(Vec>>), +} + +/// One selected name in an import selector. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct SelectedName<'db> { + /// Imported item name. + pub name: SpannedElem<'db, Ident<'db>>, + /// Optional local alias. + pub alias: Option>>, + /// Optional constructor selection for data types. + pub constructors: Option>, + /// Whether `name` came from an operator selector. + pub is_operator: bool, +} + +/// Name hidden from an import. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ImportHiddenName<'db> { + /// Hidden item name. + pub name: SpannedElem<'db, Ident<'db>>, + /// Whether `name` came from an operator selector. + pub is_operator: bool, +} + +/// Import selector. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ImportSelector<'db> { + /// Import all exported names from the target module. + Wildcard, + /// Import only the listed names. + Names(Vec>), +} + +/// Module import declaration. +/// +/// Plain imports and selected imports bind public items directly. An explicit +/// namespace alias binds a module name. External library roots use an `@` +/// prefix. #[salsa::tracked(debug)] pub struct Import<'db> { + /// Stable structural identity of the import. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full import declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] #[returns(ref)] - path: Vec>>, + pub leading_comments: Vec, + + /// Span of the external-library marker when present. + #[tracked] + #[returns(copy)] + pub external: Option>, + + /// Module path segments in source order. + #[tracked] + #[returns(ref)] + pub path: Vec>>, + + /// Optional module alias. + #[tracked] + pub alias: Option>>, + + /// Optional selected-import list. + #[tracked] + #[returns(ref)] + pub selector: Option>, + + /// Names hidden from the import. + #[tracked] + #[returns(ref)] + pub hiding: Vec>, } impl<'db> Spanned<'db> for Import<'db> { @@ -280,22 +850,94 @@ impl<'db> Spanned<'db> for Import<'db> { } } +/// One exported name in an export declaration. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ExportedName<'db> { + /// Exported item name. + pub name: SpannedElem<'db, Ident<'db>>, + /// Optional constructor selection for data types. + pub constructors: Option>, + /// Whether `name` came from an operator selector. + pub is_operator: bool, +} + +/// Export declaration payload. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum ExportKind<'db> { + /// Explicit export list from the current module. + List(Vec>), + /// Re-export every public item from the named module. + Module(Vec>>), + /// Re-export a module under an alias. + ModuleAs( + /// Source module path. + Vec>>, + /// Exported alias. + SpannedElem<'db, Ident<'db>>, + ), + /// Re-export selected items from a module. + ItemsFrom(Vec>>, Vec>), +} + +/// Module export declaration. +#[salsa::tracked(debug)] +pub struct Export<'db> { + /// Stable structural identity of the export. + #[tracked] + #[returns(copy)] + pub def_id: DefId<'db>, + + /// Span covering the full export declaration. + #[tracked] + #[returns(copy)] + pub span: Span<'db>, + + /// Consecutive source comments directly leading this declaration. + #[tracked] + #[returns(ref)] + pub leading_comments: Vec, + + /// Export payload. + #[tracked] + #[returns(ref)] + pub kind: ExportKind<'db>, +} + +impl<'db> Spanned<'db> for Export<'db> { + fn span(&self, db: &'db dyn Db) -> Span<'db> { + Export::span(*self, db) + } +} + +/// Pragma declaration. +/// +/// Pragmas are parsed and preserved in HIR so later phases can opt into +/// pragma-specific behavior without reparsing source text. #[salsa::tracked(debug)] pub struct Pragma<'db> { + /// Stable structural identity of the pragma. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the full pragma declaration. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Consecutive source comments directly leading this declaration. #[tracked] - name: SpannedElem<'db, Ident<'db>>, + #[returns(ref)] + pub leading_comments: Vec, + + /// Pragma name. + #[tracked] + pub name: SpannedElem<'db, Ident<'db>>, + /// Pragma arguments/items in source order. #[tracked] #[returns(ref)] - items: Vec>>, + pub items: Vec>>, } impl<'db> Spanned<'db> for Pragma<'db> { @@ -304,18 +946,54 @@ impl<'db> Spanned<'db> for Pragma<'db> { } } -/// Top-level item. +/// Top-level module item. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum Item<'db> { + /// Function declaration or definition. FunctionDef(FunctionDef<'db>), + /// Type alias declaration. TypeAlias(TypeAlias<'db>), + /// Algebraic data type declaration. AdtDef(AdtDef<'db>), + /// Type class declaration. ClassDef(ClassDef<'db>), + /// Type class instance declaration. InstanceDef(InstanceDef<'db>), + /// Contract declaration. ContractDef(ContractDef<'db>), + /// Import declaration. Import(Import<'db>), + /// Export declaration. + Export(Export<'db>), + /// Pragma declaration. Pragma(Pragma<'db>), - Error, + /// Parser recovery placeholder. + Error { + /// Span covering the recovered invalid top-level item. + span: Span<'db>, + /// Consecutive source comments directly leading the invalid item. + leading_comments: SourceComments<'db>, + }, +} + +impl<'db> Item<'db> { + /// Returns consecutive source comments directly leading this item. + pub fn leading_comments(self, db: &'db dyn Db) -> &'db [SourceComment] { + match self { + Self::FunctionDef(def) => def.leading_comments(db), + Self::TypeAlias(def) => def.leading_comments(db), + Self::AdtDef(def) => def.leading_comments(db), + Self::ClassDef(def) => def.leading_comments(db), + Self::InstanceDef(def) => def.leading_comments(db), + Self::ContractDef(def) => def.leading_comments(db), + Self::Import(def) => def.leading_comments(db), + Self::Export(def) => def.leading_comments(db), + Self::Pragma(def) => def.leading_comments(db), + Self::Error { + leading_comments, .. + } => leading_comments.comments(db), + } + } } impl<'db> Spanned<'db> for Item<'db> { @@ -328,26 +1006,34 @@ impl<'db> Spanned<'db> for Item<'db> { Self::InstanceDef(def) => def.span(db), Self::ContractDef(def) => def.span(db), Self::Import(def) => def.span(db), + Self::Export(def) => def.span(db), Self::Pragma(def) => def.span(db), - Self::Error => panic!("Item::Error has no span"), + Self::Error { span, .. } => *span, } } } /// A module/source file after lowering into HIR. +/// +/// A module is itself a definition so item identity can be rooted in an owner +/// chain. The module span is rooted at the source file, while child definitions +/// usually use def anchors. #[salsa::tracked(debug)] pub struct Module<'db> { + /// Stable structural identity of the module. #[tracked] #[returns(copy)] - def_id: DefId<'db>, + pub def_id: DefId<'db>, + /// Span covering the source file contents. #[tracked] #[returns(copy)] - span: Span<'db>, + pub span: Span<'db>, + /// Top-level items in source order. #[tracked] #[returns(ref)] - items: Vec>, + pub items: Vec>, } impl<'db> Spanned<'db> for Module<'db> { @@ -355,3 +1041,10 @@ impl<'db> Spanned<'db> for Module<'db> { Module::span(*self, db) } } + +impl<'db> Module<'db> { + /// Returns the stable definition identity for this module. + pub fn def_id_value(&self, db: &'db dyn Db) -> DefId<'db> { + Module::def_id(*self, db) + } +} diff --git a/crates/hir/src/ast/ty.rs b/crates/hir/src/ast/ty.rs index 8d619d61..845b2393 100644 --- a/crates/hir/src/ast/ty.rs +++ b/crates/hir/src/ast/ty.rs @@ -1,14 +1,70 @@ +//! Unresolved type and predicate syntax in HIR. +//! +//! These nodes preserve source-level type names and argument structure before +//! name resolution and type checking. A span-free source structural shape is +//! interned separately from occurrence spans so equivalent type references +//! share the same intern key even when they appear at different byte offsets. + use crate::{ Db, - ast::Ident, + ast::{Ident, function::FunctionMutability}, span::{Span, Spanned, SpannedElem}, }; -/// Unresolved type reference. -#[salsa::interned(debug)] +/// Source-level visibility qualifier on a function type. +/// +/// Function types admit only `internal` and `external` in the source grammar. +/// Declaration-only `public` and `private` states are intentionally +/// unrepresentable here. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum FunctionTypeVisibility { + /// Source `internal` qualifier. + Internal, + /// Source `external` qualifier. + External, +} + +impl FunctionTypeVisibility { + /// Returns the canonical source keyword. + pub const fn keyword(self) -> &'static str { + match self { + Self::Internal => "internal", + Self::External => "external", + } + } +} + +/// Unresolved type reference occurrence. +/// +/// A `TypeRef` names source syntax, not a resolved semantic type. Name +/// resolution maps named references to definitions, builtins, or type variables +/// later while keeping occurrence spans available for diagnostics. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct TypeRef<'db> { - #[returns(ref)] - kind: TypeRefKind<'db>, + shape: TypeRefShape<'db>, + occurrence: TypeRefOccurrence<'db>, +} + +impl<'db> TypeRef<'db> { + /// Creates a type reference from its occurrence-level syntax. + pub fn new(db: &'db dyn Db, kind: TypeRefKind<'db>) -> Self { + let shape = TypeRefShape::new(db, type_shape_from_occurrence(&kind)); + let occurrence = TypeRefOccurrence::new(db, kind); + Self { shape, occurrence } + } + + /// Returns the source occurrence shape, including spans. + pub fn kind(self, db: &'db dyn Db) -> &'db TypeRefKind<'db> { + self.occurrence.kind(db) + } + + /// Returns the span-free interned source structural shape. + /// + /// Despite the historical method name, this is unresolved source syntax, + /// not the checked semantic type used by type inference. + pub fn semantic_shape(self) -> TypeRefShape<'db> { + self.shape + } } impl<'db> Spanned<'db> for TypeRef<'db> { @@ -17,43 +73,261 @@ impl<'db> Spanned<'db> for TypeRef<'db> { } } +/// Interned source structural type-reference shape without occurrence spans. +/// +/// This preserves unresolved source distinctions and is separate from the +/// checked semantic type representation. +#[salsa::interned(debug)] +pub struct TypeRefShape<'db> { + /// Span-free source type structure. + #[returns(ref)] + pub kind: TypeRefShapeKind<'db>, +} + +/// Span-free source structural shape of an unresolved type reference. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum TypeRefShapeKind<'db> { + /// Named type constructor with optional qualifier and type arguments. + Named { + /// Qualifier path collapsed into a dotted identifier, if present. + qualifier: Option>, + /// Final type constructor name. + name: Ident<'db>, + /// Type arguments. + args: Vec>, + }, + /// Fixed-length array type. + FixedArray { + /// Array element shape. + element: TypeRefShape<'db>, + /// Strictly positive element count. + length: u64, + }, + /// Function type from parameter types to a return type. + Fn { + /// Parameter type shapes. + params: Vec>, + /// Explicit function-type visibility qualifier. + visibility: Option, + /// Explicit function-type state-mutability qualifier. + mutability: Option, + /// Return type shape. + ret: TypeRefShape<'db>, + }, + /// `comptime` type wrapper. + Comptime { + /// Wrapped type shape. + inner: TypeRefShape<'db>, + }, + /// Tuple type, including unit when the element list is empty. + Tuple { + /// Tuple element shapes. + elems: Vec>, + }, + /// Parser recovery placeholder. + Error, +} + +#[salsa::interned(debug)] +struct TypeRefOccurrence<'db> { + /// Occurrence-level syntax for the type reference. + #[returns(ref)] + kind: TypeRefKind<'db>, +} + +/// Shape of an unresolved type reference occurrence. +/// +/// Every variant carries enough span information to report errors at the syntax +/// that introduced it. `Error` is a silent recovery sentinel; parse diagnostics +/// are emitted elsewhere. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum TypeRefKind<'db> { + /// Named type constructor with optional qualifier and type arguments. Named { + /// Qualifier path collapsed into a dotted identifier, if present. + qualifier: Option>>, + /// Final type constructor name. name: SpannedElem<'db, Ident<'db>>, + /// Argument list and its source span. args: SpannedElem<'db, Vec>>, }, + /// Solidity fixed-length array type: `Element[N]`. + FixedArray { + /// Array element type. + element: TypeRef<'db>, + /// Strictly positive element count. + length: u64, + /// Span of the complete `[N]` suffix. + brackets: Span<'db>, + }, + /// Function type from parameter types to a return type. Fn { + /// Span covering the complete function type, beginning at `function`. + span: Span<'db>, + /// Parameter type list and the span of the parameter group. params: SpannedElem<'db, Vec>>, + /// Explicit visibility qualifier and its keyword span. + visibility: Option>, + /// Explicit state-mutability qualifier and its keyword span. + mutability: Option>, + /// Return type. ret: TypeRef<'db>, }, + /// `comptime` type wrapper. + Comptime { + /// Span of the `comptime` keyword. + kw: Span<'db>, + /// Wrapped type. + inner: TypeRef<'db>, + }, + /// Tuple type, including unit when the element list is empty. Tuple { - elems: SpannedElem<'db, TypeRef<'db>>, + /// Tuple elements and span of the tuple syntax. + elems: SpannedElem<'db, Vec>>, + }, + /// Parser recovery placeholder. + Error { + /// Span covering the unparseable type syntax. + span: Span<'db>, }, - Error, } impl<'db> Spanned<'db> for TypeRefKind<'db> { fn span(&self, db: &'db dyn Db) -> Span<'db> { match self { - Self::Named { name, args } => name.span(db) + args.span(db), - Self::Fn { params, ret } => params.span(db) + ret.span(db), + Self::Named { + qualifier, + name, + args, + } => { + let head = qualifier + .as_ref() + .map(|qualifier| qualifier.span(db) + name.span(db)) + .unwrap_or_else(|| name.span(db)); + head + args.span(db) + } + Self::FixedArray { + element, brackets, .. + } => element.span(db) + *brackets, + Self::Fn { span, .. } => *span, + Self::Comptime { kw, inner } => *kw + inner.span(db), Self::Tuple { elems } => elems.span(db), - Self::Error => panic!("TypeRefKind::Error has no span"), + Self::Error { span } => *span, } } } -#[salsa::interned(debug)] +fn type_shape_from_occurrence<'db>(kind: &TypeRefKind<'db>) -> TypeRefShapeKind<'db> { + match kind { + TypeRefKind::Named { + qualifier, + name, + args, + } => TypeRefShapeKind::Named { + qualifier: qualifier.as_ref().map(|it| *it.atom()), + name: *name.atom(), + args: args.atom().iter().map(|arg| arg.semantic_shape()).collect(), + }, + TypeRefKind::FixedArray { + element, length, .. + } => TypeRefShapeKind::FixedArray { + element: element.semantic_shape(), + length: *length, + }, + TypeRefKind::Fn { + params, + visibility, + mutability, + ret, + .. + } => TypeRefShapeKind::Fn { + params: params + .atom() + .iter() + .map(|param| param.semantic_shape()) + .collect(), + visibility: visibility.map(|visibility| *visibility.atom()), + mutability: mutability.map(|mutability| *mutability.atom()), + ret: ret.semantic_shape(), + }, + TypeRefKind::Comptime { inner, .. } => TypeRefShapeKind::Comptime { + inner: inner.semantic_shape(), + }, + TypeRefKind::Tuple { elems } => TypeRefShapeKind::Tuple { + elems: elems + .atom() + .iter() + .map(|elem| elem.semantic_shape()) + .collect(), + }, + TypeRefKind::Error { .. } => TypeRefShapeKind::Error, + } +} + +/// Unresolved class predicate reference occurrence. +/// +/// Predicates bind a main type to a class and optional class arguments, for +/// example `T: Int` or `T: Class(U)`. The class name is resolved separately +/// from the participating type references. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct PredRef<'db> { + shape: PredRefShape<'db>, + occurrence: PredRefOccurrence<'db>, +} + +impl<'db> PredRef<'db> { + /// Creates a predicate reference from its occurrence-level syntax. + pub fn new(db: &'db dyn Db, kind: PredRefKind<'db>) -> Self { + let shape = PredRefShape::new(db, pred_shape_from_occurrence(&kind)); + let occurrence = PredRefOccurrence::new(db, kind); + Self { shape, occurrence } + } + + /// Returns the source occurrence shape, including spans. + pub fn kind(self, db: &'db dyn Db) -> &'db PredRefKind<'db> { + self.occurrence.kind(db) + } + + /// Returns the span-free interned semantic shape. + pub fn semantic_shape(self) -> PredRefShape<'db> { + self.shape + } +} + +/// Interned semantic predicate reference shape without occurrence spans. +#[salsa::interned(debug)] +pub struct PredRefShape<'db> { + /// Span-free predicate structure. + #[returns(ref)] + pub kind: PredRefShapeKind<'db>, +} + +/// Span-free shape of an unresolved predicate reference. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct PredRefShapeKind<'db> { + /// Main type being constrained. + pub ty: TypeRefShape<'db>, + /// Class name used by the predicate. + pub class: Ident<'db>, + /// Additional class argument shapes. + pub args: Vec>, +} + +#[salsa::interned(debug)] +struct PredRefOccurrence<'db> { + /// Occurrence-level predicate syntax. #[returns(ref)] kind: PredRefKind<'db>, } +/// Source-level class predicate syntax. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub struct PredRefKind<'db> { + /// Main type being constrained. pub ty: TypeRef<'db>, + /// Class name used by the predicate. pub class: SpannedElem<'db, Ident<'db>>, + /// Additional class arguments and their list span. pub args: SpannedElem<'db, Vec>>, } @@ -68,3 +342,16 @@ impl<'db> Spanned<'db> for PredRef<'db> { self.kind(db).span(db) } } + +fn pred_shape_from_occurrence<'db>(kind: &PredRefKind<'db>) -> PredRefShapeKind<'db> { + PredRefShapeKind { + ty: kind.ty.semantic_shape(), + class: *kind.class.atom(), + args: kind + .args + .atom() + .iter() + .map(|arg| arg.semantic_shape()) + .collect(), + } +} diff --git a/crates/hir/src/diag.rs b/crates/hir/src/diag.rs deleted file mode 100644 index 3a3662c3..00000000 --- a/crates/hir/src/diag.rs +++ /dev/null @@ -1,516 +0,0 @@ -use annotate_snippets::{Annotation, AnnotationKind, Group, Level, Renderer, Snippet}; -use salsa::Accumulator; - -use crate::{ - anchor::{DefId, DefKey, def_locations_for_file, resolve_def_location}, - input::SourceFile, - span::{AnchorKind, Span}, -}; - -/// A diagnostic emitted during compilation. -#[salsa::accumulator] -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Diagnostic { - /// Severity of this diagnostic. - pub level: DiagnosticLevel, - /// Human-readable headline message. - pub message: String, - /// Optional diagnostic code, e.g. `E0001`. - pub code: Option, - /// Source labels to render with this diagnostic. - pub labels: Vec, - /// Additional notes/help text shown below the main message. - pub notes: Vec, -} - -/// Severity level for diagnostics. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum DiagnosticLevel { - Error, - Warning, - Note, - Help, -} - -/// Lifetime-free anchor used by diagnostics. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -enum LabelAnchor { - Root(SourceFile), - Def(DefKey), -} - -/// Lifetime-free span snapshot stored in diagnostics. -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] -struct LabelSpan { - anchor: LabelAnchor, - begin: Offset, - end: Offset, -} - -impl LabelSpan { - fn new(anchor: LabelAnchor, begin: Offset, end: Offset) -> Self { - assert!(begin <= end, "span start must be <= end"); - Self { anchor, begin, end } - } - - fn from_span<'db>(db: &'db dyn crate::Db, span: Span<'db>) -> Self { - let anchor = match span.anchor().kind_value(db) { - AnchorKind::Root(file) => LabelAnchor::Root(file), - AnchorKind::Def(def) => LabelAnchor::Def(def.key(db)), - }; - Self::new(anchor, span.begin(), span.end()) - } - - fn resolve_to_absolute(&self, db: &dyn crate::Db) -> AbsoluteSpan { - let (file, base) = match &self.anchor { - LabelAnchor::Root(file) => (*file, Offset::new(0)), - LabelAnchor::Def(key) => { - let table = def_locations_for_file(db, key.file); - let def = DefId::from_key(db, key); - let loc = resolve_def_location(table, def) - .unwrap_or_else(|| panic!("missing DefLocation for def key: {:?}", key)); - (loc.file, loc.base_offset) - } - }; - AbsoluteSpan::new( - file, - add_offset(base, self.begin), - add_offset(base, self.end), - ) - } -} - -/// Span label attached to a diagnostic. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct DiagnosticLabel { - /// Where this label points to in source. - span: LabelSpan, - /// Optional message displayed for this label. - message: Option, - /// Label style used by renderers (primary/secondary). - style: LabelStyle, -} - -/// Proof token that a diagnostic has been accumulated. -#[must_use] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct AccumulatedProof { - _private: (), -} - -/// Style of a diagnostic label. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum LabelStyle { - Primary, - Secondary, -} - -/// Byte offset into a source file. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, salsa::Update)] -pub struct Offset(u32); - -impl Offset { - /// Creates an offset from a raw `u32` byte index. - pub const fn new(raw: u32) -> Self { - Self(raw) - } - - /// Returns this offset as a `u32` byte index. - pub const fn as_u32(self) -> u32 { - self.0 - } - - /// Returns this offset as a `usize` byte index. - pub fn as_usize(self) -> usize { - self.0 as usize - } - - /// Tries to create an offset from `usize`. - pub fn try_from_usize(raw: usize) -> Option { - u32::try_from(raw).ok().map(Self) - } -} - -/// Span represented as absolute offsets in a specific file. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct AbsoluteSpan { - pub file: SourceFile, - pub start: Offset, - pub end: Offset, -} - -impl AbsoluteSpan { - /// Creates a new absolute span. - /// - /// Panics if `start > end`. - pub fn new(file: SourceFile, start: Offset, end: Offset) -> Self { - assert!(start <= end, "span start must be <= end"); - Self { file, start, end } - } - - /// Returns the file this span belongs to. - pub const fn file(self) -> SourceFile { - self.file - } - - /// Returns the start byte offset. - pub const fn start(self) -> Offset { - self.start - } - - /// Returns the end byte offset. - pub const fn end(self) -> Offset { - self.end - } - - /// Returns span length in bytes. - pub fn len(self) -> u32 { - self.end.as_u32() - self.start.as_u32() - } - - /// Returns `true` when the span is empty. - pub fn is_empty(self) -> bool { - self.start == self.end - } -} - -impl Diagnostic { - /// Creates a new diagnostic with the given severity and message. - pub fn new(level: DiagnosticLevel, message: impl Into) -> Self { - Self { - level, - message: message.into(), - code: None, - labels: Vec::new(), - notes: Vec::new(), - } - } - - /// Creates an error diagnostic. - pub fn error(message: impl Into) -> Self { - Self::new(DiagnosticLevel::Error, message) - } - - /// Creates a warning diagnostic. - pub fn warning(message: impl Into) -> Self { - Self::new(DiagnosticLevel::Warning, message) - } - - /// Creates a note diagnostic. - pub fn note(message: impl Into) -> Self { - Self::new(DiagnosticLevel::Note, message) - } - - /// Creates a help diagnostic. - pub fn help(message: impl Into) -> Self { - Self::new(DiagnosticLevel::Help, message) - } - - /// Adds a diagnostic code. - pub fn with_code(mut self, code: impl Into) -> Self { - self.code = Some(code.into()); - self - } - - /// Appends a label. - pub fn with_label(mut self, label: DiagnosticLabel) -> Self { - self.labels.push(label); - self - } - - /// Appends a primary label. - fn with_primary_label_span(self, span: LabelSpan, message: Option>) -> Self { - self.with_label(DiagnosticLabel::primary(span, message)) - } - - /// Appends a primary label. - pub fn with_primary_label<'db>( - self, - db: &'db dyn crate::Db, - span: Span<'db>, - message: Option>, - ) -> Self { - self.with_primary_label_span(LabelSpan::from_span(db, span), message) - } - - /// Appends a secondary label. - fn with_secondary_label_span( - self, - span: LabelSpan, - message: Option>, - ) -> Self { - self.with_label(DiagnosticLabel::secondary(span, message)) - } - - /// Appends a secondary label. - pub fn with_secondary_label<'db>( - self, - db: &'db dyn crate::Db, - span: Span<'db>, - message: Option>, - ) -> Self { - self.with_secondary_label_span(LabelSpan::from_span(db, span), message) - } - - /// Appends a note/help text line. - pub fn with_note(mut self, note: impl Into) -> Self { - self.notes.push(note.into()); - self - } - - /// Accumulate this diagnostic and returns proof that reporting happened. - pub fn accumulate(self, db: &dyn crate::Db) -> AccumulatedProof { - ::accumulate(self, db); - AccumulatedProof { _private: () } - } - - /// Converts this diagnostic into an `annotate_snippets` report. - pub fn to_annotate_report<'db>(&self, db: &'db dyn crate::Db) -> Vec> { - let mut title = self - .level - .to_annotate_level() - .primary_title(self.message.clone()); - if let Some(code) = &self.code { - title = title.id(code.clone()); - } - - let mut group = Group::with_title(title); - - let mut by_file: Vec<(SourceFile, Vec<(&DiagnosticLabel, AbsoluteSpan)>)> = Vec::new(); - for label in &self.labels { - let absolute = label.span.resolve_to_absolute(db); - let file = absolute.file(); - if let Some((_, labels)) = by_file - .iter_mut() - .find(|(existing_file, _)| *existing_file == file) - { - labels.push((label, absolute)); - } else { - by_file.push((file, vec![(label, absolute)])); - } - } - - for (file, labels) in by_file { - let url = file.url(db); - let Some(content) = file.content(db) else { - continue; - }; - - let source_len = content.len(); - let mut annotations: Vec> = Vec::with_capacity(labels.len()); - let mut visible_ranges = Vec::with_capacity(labels.len()); - - for (label, absolute) in labels { - let span = clamp_span( - absolute.start().as_usize(), - absolute.end().as_usize(), - source_len, - ); - visible_ranges.push(context_window_span(content.as_str(), &span, 1, 1)); - let mut annotation = label.style.to_annotate_kind().span(span); - if let Some(message) = &label.message { - annotation = annotation.label(message.clone()); - } - if matches!(label.style, LabelStyle::Primary) { - annotation = annotation.highlight_source(true); - } - annotations.push(annotation); - } - - let mut snippet = Snippet::source(content).path(url.path()); - for range in merge_ranges(visible_ranges) { - snippet = snippet.annotation(AnnotationKind::Visible.span(range)); - } - snippet = snippet.annotations(annotations); - - group = group.element(snippet); - } - - for note in &self.notes { - group = group.element(Level::NOTE.message(note.clone())); - } - - vec![group] - } - - /// Renders this diagnostic using the default styled renderer. - pub fn render(&self, db: &dyn crate::Db) -> String { - self.render_with(db, &Renderer::styled()) - } - - /// Renders this diagnostic using the provided `annotate_snippets` renderer. - pub fn render_with(&self, db: &dyn crate::Db, renderer: &Renderer) -> String { - let report = self.to_annotate_report(db); - renderer.render(&report) - } -} - -impl DiagnosticLabel { - /// Creates a new diagnostic label. - fn new(span: LabelSpan, style: LabelStyle, message: Option>) -> Self { - Self { - span, - style, - message: message.map(Into::into), - } - } - - /// Creates a primary label. - fn primary(span: LabelSpan, message: Option>) -> Self { - Self::new(span, LabelStyle::Primary, message) - } - - /// Creates a secondary label. - fn secondary(span: LabelSpan, message: Option>) -> Self { - Self::new(span, LabelStyle::Secondary, message) - } -} - -impl DiagnosticLevel { - fn to_annotate_level(self) -> Level<'static> { - match self { - DiagnosticLevel::Error => Level::ERROR, - DiagnosticLevel::Warning => Level::WARNING, - DiagnosticLevel::Note => Level::NOTE, - DiagnosticLevel::Help => Level::HELP, - } - } -} - -impl LabelStyle { - fn to_annotate_kind(self) -> AnnotationKind { - match self { - LabelStyle::Primary => AnnotationKind::Primary, - LabelStyle::Secondary => AnnotationKind::Context, - } - } -} - -fn clamp_span(start: usize, end: usize, source_len: usize) -> core::ops::Range { - let start = start.min(source_len); - let end = end.min(source_len); - if start <= end { start..end } else { end..start } -} - -fn context_window_span( - source: &str, - focus: &core::ops::Range, - lines_before: usize, - lines_after: usize, -) -> core::ops::Range { - if source.is_empty() { - return 0..0; - } - - let focus_start = normalize_line_lookup_offset(source, focus.start); - let focus_end = normalize_line_lookup_offset(source, focus.end); - - let mut start = line_start_at_or_before(source, focus_start); - for _ in 0..lines_before { - if start == 0 { - break; - } - start = line_start_at_or_before(source, start.saturating_sub(1)); - } - - let mut end = line_end_at_or_after(source, focus_end); - for _ in 0..lines_after { - if end >= source.len() { - break; - } - end = line_end_at_or_after(source, (end + 1).min(source.len())); - } - - let target_lines = lines_before + lines_after + 1; - while count_lines_in_span(source, start, end) < target_lines { - if start > 0 { - start = line_start_at_or_before(source, start.saturating_sub(1)); - continue; - } - if end < source.len() { - end = line_end_at_or_after(source, (end + 1).min(source.len())); - } else { - break; - } - } - - if start == end && !source.is_empty() { - start..(end + 1).min(source.len()) - } else { - start..end - } -} - -fn normalize_line_lookup_offset(source: &str, offset: usize) -> usize { - let mut offset = offset.min(source.len()); - if offset == source.len() { - offset = offset.saturating_sub(1); - } - let bytes = source.as_bytes(); - if bytes.get(offset).copied() == Some(b'\n') && offset > 0 { - offset -= 1; - } - offset -} - -fn line_start_at_or_before(source: &str, offset: usize) -> usize { - let offset = offset.min(source.len()); - source[..offset].rfind('\n').map_or(0, |idx| idx + 1) -} - -fn line_end_at_or_after(source: &str, offset: usize) -> usize { - let offset = offset.min(source.len()); - source[offset..] - .find('\n') - .map_or(source.len(), |idx| offset + idx) -} - -fn merge_ranges(mut ranges: Vec>) -> Vec> { - if ranges.len() <= 1 { - return ranges; - } - - ranges.sort_by_key(|range| (range.start, range.end)); - let mut merged: Vec> = Vec::with_capacity(ranges.len()); - for range in ranges { - if let Some(last) = merged.last_mut() { - if range.start <= last.end { - if range.end > last.end { - last.end = range.end; - } - } else { - merged.push(range); - } - } else { - merged.push(range); - } - } - merged -} - -fn count_lines_in_span(source: &str, start: usize, end: usize) -> usize { - if source.is_empty() { - return 0; - } - let start = start.min(source.len()); - let end = end.min(source.len()); - if start >= end { - return 1; - } - let mut count = source[start..end] - .bytes() - .filter(|byte| *byte == b'\n') - .count() - + 1; - if end == source.len() && source.ends_with('\n') && count > 0 { - count -= 1; - } - count -} - -fn add_offset(base: Offset, rel: Offset) -> Offset { - let Some(raw) = base.as_u32().checked_add(rel.as_u32()) else { - panic!("offset overflow while resolving diagnostic span"); - }; - Offset::new(raw) -} diff --git a/crates/hir/src/diag/code.rs b/crates/hir/src/diag/code.rs new file mode 100644 index 00000000..b70aff58 --- /dev/null +++ b/crates/hir/src/diag/code.rs @@ -0,0 +1,533 @@ +/// Registry of compiler diagnostic code strings. +/// +/// The associated constants are the single source for phase diagnostic codes. +/// Some constants intentionally share a value to preserve historical aliases +/// across phases; those aliases are documented in +/// [`DiagnosticCode::INTENTIONAL_DUPLICATES`]. +pub struct DiagnosticCode; + +/// One named diagnostic-code registry entry. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DiagnosticCodeEntry { + name: &'static str, + code: &'static str, +} + +/// One explicitly documented duplicate diagnostic-code value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DiagnosticCodeAlias { + code: &'static str, + reason: &'static str, +} + +impl DiagnosticCodeEntry { + const fn new(name: &'static str, code: &'static str) -> Self { + Self { name, code } + } + + /// Symbolic registry name for the diagnostic. + pub const fn name(self) -> &'static str { + self.name + } + + /// User-facing diagnostic code string. + pub const fn code(self) -> &'static str { + self.code + } +} + +impl DiagnosticCodeAlias { + const fn new(code: &'static str, reason: &'static str) -> Self { + Self { code, reason } + } + + /// User-facing diagnostic code string that is intentionally reused. + pub const fn code(self) -> &'static str { + self.code + } + + /// Human-readable reason for the alias. + pub const fn reason(self) -> &'static str { + self.reason + } +} + +impl DiagnosticCode { + /// Parser or lowering failed before semantic analysis. + pub const PARSE_ERROR: &'static str = "SC0001"; + + pub const NAMERES_UNDEFINED_NAME: &'static str = "SC0101"; + pub const TYPECK_UNDEFINED_TYPE_VARIABLES: &'static str = "SC0102"; + pub const NAMERES_UNDEFINED_TYPE_CONSTRUCTOR: &'static str = "SC0103"; + pub const NAMERES_UNDEFINED_CLASS: &'static str = "SC0105"; + pub const NAMERES_UNQUALIFIED_CONSTRUCTOR: &'static str = "SC0106"; + pub const NAMERES_INVALID_PATTERN: &'static str = "SC0107"; + pub const NAMERES_DUPLICATE_DECLARATION: &'static str = "SC0108"; + pub const MODULE_NOT_FOUND: &'static str = "SC0109"; + pub const MODULE_UNKNOWN_IMPORT_ITEM: &'static str = "SC0110"; + pub const MODULE_DUPLICATE_EXPORTED_ITEM_NAME: &'static str = "SC0111"; + pub const MODULE_DUPLICATE_EXPORTED_MODULE_NAME: &'static str = "SC0112"; + pub const MODULE_UNKNOWN_LOCAL_EXPORT: &'static str = "SC0113"; + pub const MODULE_UNKNOWN_LOCAL_CONSTRUCTOR: &'static str = "SC0114"; + pub const MODULE_UNKNOWN_REEXPORT: &'static str = "SC0115"; + pub const MODULE_UNKNOWN_REEXPORT_CONSTRUCTOR: &'static str = "SC0115"; + pub const MODULE_DUPLICATE_IMPORT_QUALIFIER: &'static str = "SC0116"; + pub const MODULE_DUPLICATE_IMPORT_SELECTOR: &'static str = "SC0117"; + pub const MODULE_MISSING_EXTERNAL_ROOT: &'static str = "SC0118"; + pub const MODULE_AMBIGUOUS_SELECTED_IMPORT: &'static str = "SC0120"; + pub const MODULE_CONFLICTING_UNQUALIFIED_NAME: &'static str = "SC0121"; + + pub const TYPECK_MISMATCH: &'static str = "SC0201"; + pub const TYPECK_RECURSIVE_TYPE_OR_UNKNOWN_INSTANCE_METHOD: &'static str = "SC0202"; + pub const TYPECK_WRONG_ARITY: &'static str = "SC0203"; + pub const TYPECK_MUTUAL_RECURSIVE_DATA: &'static str = "SC0203"; + pub const TYPECK_NON_WORD_YUL_VAR: &'static str = "SC0204"; + pub const TYPECK_UNKNOWN_FIELD: &'static str = "SC0205"; + pub const TYPECK_NON_CALLABLE: &'static str = "SC0206"; + pub const TYPECK_UNSATISFIED_CONSTRAINT: &'static str = "SC0207"; + pub const TYPECK_AMBIGUOUS_CONSTRAINT: &'static str = "SC0208"; + pub const TYPECK_SOLVER_FUEL_EXHAUSTED: &'static str = "SC0209"; + pub const TYPECK_UNKNOWN_YUL_NAME: &'static str = "SC0211"; + pub const TYPECK_COVERAGE_CONDITION: &'static str = "SC0212"; + pub const TYPECK_PATTERSON_CONDITION: &'static str = "SC0213"; + pub const TYPECK_BOUNDED_VARIABLE_CONDITION: &'static str = "SC0214"; + pub const TYPECK_TYPE_ALIAS_CYCLE: &'static str = "SC0215"; + pub const TYPECK_TYPE_ALIAS_ARITY: &'static str = "SC0216"; + pub const TYPECK_CLASS_ARITY: &'static str = "SC0217"; + pub const TYPECK_OVERLAPPING_INSTANCE: &'static str = "SC0218"; + pub const TYPECK_INVALID_DEFAULT_INSTANCE: &'static str = "SC0219"; + pub const TYPECK_INCOMPLETE_SIGNATURE: &'static str = "SC0220"; + pub const TYPECK_INCOMPLETE_METHOD_SIGNATURE: &'static str = "SC0221"; + pub const TYPECK_INVALID_INSTANCE_METHOD_SIGNATURE: &'static str = "SC0221"; + pub const TYPECK_NON_FINAL_RETURN_OR_INVALID_CONSTRUCTOR_PATTERN: &'static str = "SC0222"; + pub const TYPECK_HIDDEN_CONSTRUCTOR_COVERAGE: &'static str = "SC0223"; + pub const TYPECK_SHORTHAND_CONSTRUCTOR: &'static str = "SC0224"; + pub const TYPECK_GENERIC_DERIVE_CONFLICT: &'static str = "SC0227"; + pub const TYPECK_NAMESPACE_AS_VALUE: &'static str = "SC0228"; + pub const TYPECK_CLASS_AS_TYPE: &'static str = "SC0229"; + pub const TYPECK_DUPLICATE_TYPE: &'static str = "SC0229"; + pub const TYPECK_CONTRACT_SELECTOR_COLLISION: &'static str = "SC0235"; + pub const TYPECK_CONTRACT_RUNTIME_MAIN_ARITY: &'static str = "SC0236"; + pub const TYPECK_RUNTIME_TO_COMPTIME_PARAM: &'static str = "SC0240"; + pub const TYPECK_COMPTIME_LET_RUNTIME: &'static str = "SC0241"; + pub const TYPECK_COMPTIME_RETURN_RUNTIME: &'static str = "SC0242"; + pub const TYPECK_TYPE_ALIAS_EXPANSION_LIMIT: &'static str = "SC0243"; + pub const TYPECK_INCOMPLETE_INSTANCE: &'static str = "SC0244"; + pub const TYPECK_INVALID_CONVERSION: &'static str = "SC0245"; + pub const TYPECK_UNSUPPORTED_STORAGE_FIELD_PROJECTION: &'static str = "SC0246"; + pub const TYPECK_INVALID_VALUE_TYPE_DEFINITION: &'static str = "SC0247"; + pub const TYPECK_UNSUPPORTED_VALUE_TYPE_STORAGE: &'static str = "SC0248"; + pub const TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE: &'static str = "SC0249"; + pub const TYPECK_AMBIGUOUS_INFERENCE_OR_TYPE_CONSTRUCTOR_ARITY: &'static str = "SC0299"; + pub const TYPECK_NON_EXHAUSTIVE_MATCH: &'static str = "SC0302"; + pub const EMIT_NON_EXHAUSTIVE_MATCH: &'static str = "SC0302"; + pub const TYPECK_UNREACHABLE_MATCH_ARM: &'static str = "SC0303"; + pub const EMIT_EMPTY_MATCH: &'static str = "SC0303"; + + pub const SPECIALIZE_FREE_TYPE_VARIABLE: &'static str = "SC0401"; + pub const SPECIALIZE_INSTANTIATION_FUEL_EXHAUSTED: &'static str = "SC0402"; + pub const SPECIALIZE_INSTANTIATION_DEPTH_EXCEEDED: &'static str = "SC0403"; + pub const SPECIALIZE_MISSING_BODY: &'static str = "SC0404"; + pub const SPECIALIZE_MISSING_RESOLUTION: &'static str = "SC0405"; + pub const SPECIALIZE_MISSING_EVIDENCE: &'static str = "SC0406"; + pub const SPECIALIZE_UNSUPPORTED_EVIDENCE: &'static str = "SC0407"; + pub const SPECIALIZE_UNRESOLVED_EXTERNAL: &'static str = "SC0408"; + pub const SPECIALIZE_COMPTIME_EVALUATION_FAILED: &'static str = "SC0409"; + pub const SPECIALIZE_COMPTIME_FUEL_EXHAUSTED: &'static str = "SC0410"; + pub const SPECIALIZE_INTEGER_ERASURE: &'static str = "SC0411"; + pub const SPECIALIZE_TYPE_SIZE_EXCEEDED: &'static str = "SC0412"; + pub const SPECIALIZE_PUBLIC_COMPTIME_PARAM: &'static str = "SC0413"; + pub const SPECIALIZE_REDUCTION_RECURSION: &'static str = "SC0414"; + pub const SPECIALIZE_REDUCTION_FUEL_EXHAUSTED: &'static str = "SC0415"; + pub const SPECIALIZE_UNSUPPORTED_RUNTIME_TYPE: &'static str = "SC0416"; + + pub const EMIT_UNSUPPORTED_TYPE: &'static str = "SC0420"; + pub const EMIT_UNSUPPORTED_LITERAL: &'static str = "SC0421"; + pub const EMIT_UNSUPPORTED_MONO_CONSTRUCT: &'static str = "SC0422"; + pub const EMIT_MISSING_ADT_LAYOUT: &'static str = "SC0423"; + pub const EMIT_MISSING_CONSTRUCTOR: &'static str = "SC0424"; + pub const EMIT_DISPATCHER_DEFERRED: &'static str = "SC0425"; + pub const EMIT_UNSUPPORTED_DISPATCH_ENTRY: &'static str = "SC0426"; + pub const EMIT_MULTI_SCRUTINEE_MATCH: &'static str = "SC0427"; + + pub const HULL_UNDEFINED_VARIABLE: &'static str = "SC0430"; + pub const HULL_UNDEFINED_FUNCTION: &'static str = "SC0431"; + pub const HULL_DUPLICATE_FUNCTION: &'static str = "SC0432"; + pub const HULL_ARITY_MISMATCH: &'static str = "SC0433"; + pub const HULL_TYPE_MISMATCH: &'static str = "SC0434"; + pub const HULL_EXPR_ANNOTATION_MISMATCH: &'static str = "SC0435"; + pub const HULL_EXPECTED_PRODUCT: &'static str = "SC0436"; + pub const HULL_EXPECTED_SUM: &'static str = "SC0437"; + pub const HULL_EXPECTED_BOOL: &'static str = "SC0438"; + pub const HULL_BAD_INJECTION_INDEX: &'static str = "SC0439"; + pub const HULL_BAD_MATCH_PATTERN: &'static str = "SC0440"; + pub const HULL_RETURN_OUTSIDE_FUNCTION: &'static str = "SC0441"; + pub const HULL_FUNCTION_TYPE_NOT_FIRST_ORDER: &'static str = "SC0442"; + pub const HULL_MISSING_TERMINATOR: &'static str = "SC0443"; + pub const HULL_ASSEMBLY_REQUIRES_DATABASE: &'static str = "SC0444"; + pub const HULL_ASSEMBLY_RETURN_COUNT_MISMATCH: &'static str = "SC0445"; + pub const HULL_ASSEMBLY_EXPRESSION_NOT_UNIT: &'static str = "SC0446"; + pub const HULL_ASSEMBLY_EXPECTED_WORD_ARGUMENT: &'static str = "SC0447"; + pub const HULL_ASSEMBLY_EXPECTED_WORD_ASSIGNMENT: &'static str = "SC0448"; + pub const HULL_ASSEMBLY_VOID_ARGUMENT: &'static str = "SC0449"; + + /// All named code constants. Tests enforce that duplicate values appear + /// only in [`Self::INTENTIONAL_DUPLICATES`]. + pub const ALL: &'static [DiagnosticCodeEntry] = &[ + DiagnosticCodeEntry::new("PARSE_ERROR", Self::PARSE_ERROR), + DiagnosticCodeEntry::new("NAMERES_UNDEFINED_NAME", Self::NAMERES_UNDEFINED_NAME), + DiagnosticCodeEntry::new( + "TYPECK_UNDEFINED_TYPE_VARIABLES", + Self::TYPECK_UNDEFINED_TYPE_VARIABLES, + ), + DiagnosticCodeEntry::new( + "NAMERES_UNDEFINED_TYPE_CONSTRUCTOR", + Self::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR, + ), + DiagnosticCodeEntry::new("NAMERES_UNDEFINED_CLASS", Self::NAMERES_UNDEFINED_CLASS), + DiagnosticCodeEntry::new( + "NAMERES_UNQUALIFIED_CONSTRUCTOR", + Self::NAMERES_UNQUALIFIED_CONSTRUCTOR, + ), + DiagnosticCodeEntry::new("NAMERES_INVALID_PATTERN", Self::NAMERES_INVALID_PATTERN), + DiagnosticCodeEntry::new( + "NAMERES_DUPLICATE_DECLARATION", + Self::NAMERES_DUPLICATE_DECLARATION, + ), + DiagnosticCodeEntry::new("MODULE_NOT_FOUND", Self::MODULE_NOT_FOUND), + DiagnosticCodeEntry::new( + "MODULE_UNKNOWN_IMPORT_ITEM", + Self::MODULE_UNKNOWN_IMPORT_ITEM, + ), + DiagnosticCodeEntry::new( + "MODULE_DUPLICATE_EXPORTED_ITEM_NAME", + Self::MODULE_DUPLICATE_EXPORTED_ITEM_NAME, + ), + DiagnosticCodeEntry::new( + "MODULE_DUPLICATE_EXPORTED_MODULE_NAME", + Self::MODULE_DUPLICATE_EXPORTED_MODULE_NAME, + ), + DiagnosticCodeEntry::new( + "MODULE_UNKNOWN_LOCAL_EXPORT", + Self::MODULE_UNKNOWN_LOCAL_EXPORT, + ), + DiagnosticCodeEntry::new( + "MODULE_UNKNOWN_LOCAL_CONSTRUCTOR", + Self::MODULE_UNKNOWN_LOCAL_CONSTRUCTOR, + ), + DiagnosticCodeEntry::new("MODULE_UNKNOWN_REEXPORT", Self::MODULE_UNKNOWN_REEXPORT), + DiagnosticCodeEntry::new( + "MODULE_UNKNOWN_REEXPORT_CONSTRUCTOR", + Self::MODULE_UNKNOWN_REEXPORT_CONSTRUCTOR, + ), + DiagnosticCodeEntry::new( + "MODULE_DUPLICATE_IMPORT_QUALIFIER", + Self::MODULE_DUPLICATE_IMPORT_QUALIFIER, + ), + DiagnosticCodeEntry::new( + "MODULE_DUPLICATE_IMPORT_SELECTOR", + Self::MODULE_DUPLICATE_IMPORT_SELECTOR, + ), + DiagnosticCodeEntry::new( + "MODULE_MISSING_EXTERNAL_ROOT", + Self::MODULE_MISSING_EXTERNAL_ROOT, + ), + DiagnosticCodeEntry::new( + "MODULE_AMBIGUOUS_SELECTED_IMPORT", + Self::MODULE_AMBIGUOUS_SELECTED_IMPORT, + ), + DiagnosticCodeEntry::new( + "MODULE_CONFLICTING_UNQUALIFIED_NAME", + Self::MODULE_CONFLICTING_UNQUALIFIED_NAME, + ), + DiagnosticCodeEntry::new("TYPECK_MISMATCH", Self::TYPECK_MISMATCH), + DiagnosticCodeEntry::new( + "TYPECK_RECURSIVE_TYPE_OR_UNKNOWN_INSTANCE_METHOD", + Self::TYPECK_RECURSIVE_TYPE_OR_UNKNOWN_INSTANCE_METHOD, + ), + DiagnosticCodeEntry::new("TYPECK_WRONG_ARITY", Self::TYPECK_WRONG_ARITY), + DiagnosticCodeEntry::new( + "TYPECK_MUTUAL_RECURSIVE_DATA", + Self::TYPECK_MUTUAL_RECURSIVE_DATA, + ), + DiagnosticCodeEntry::new("TYPECK_NON_WORD_YUL_VAR", Self::TYPECK_NON_WORD_YUL_VAR), + DiagnosticCodeEntry::new("TYPECK_UNKNOWN_FIELD", Self::TYPECK_UNKNOWN_FIELD), + DiagnosticCodeEntry::new("TYPECK_NON_CALLABLE", Self::TYPECK_NON_CALLABLE), + DiagnosticCodeEntry::new( + "TYPECK_UNSATISFIED_CONSTRAINT", + Self::TYPECK_UNSATISFIED_CONSTRAINT, + ), + DiagnosticCodeEntry::new( + "TYPECK_AMBIGUOUS_CONSTRAINT", + Self::TYPECK_AMBIGUOUS_CONSTRAINT, + ), + DiagnosticCodeEntry::new( + "TYPECK_SOLVER_FUEL_EXHAUSTED", + Self::TYPECK_SOLVER_FUEL_EXHAUSTED, + ), + DiagnosticCodeEntry::new("TYPECK_UNKNOWN_YUL_NAME", Self::TYPECK_UNKNOWN_YUL_NAME), + DiagnosticCodeEntry::new("TYPECK_COVERAGE_CONDITION", Self::TYPECK_COVERAGE_CONDITION), + DiagnosticCodeEntry::new( + "TYPECK_PATTERSON_CONDITION", + Self::TYPECK_PATTERSON_CONDITION, + ), + DiagnosticCodeEntry::new( + "TYPECK_BOUNDED_VARIABLE_CONDITION", + Self::TYPECK_BOUNDED_VARIABLE_CONDITION, + ), + DiagnosticCodeEntry::new("TYPECK_TYPE_ALIAS_CYCLE", Self::TYPECK_TYPE_ALIAS_CYCLE), + DiagnosticCodeEntry::new("TYPECK_TYPE_ALIAS_ARITY", Self::TYPECK_TYPE_ALIAS_ARITY), + DiagnosticCodeEntry::new( + "TYPECK_TYPE_ALIAS_EXPANSION_LIMIT", + Self::TYPECK_TYPE_ALIAS_EXPANSION_LIMIT, + ), + DiagnosticCodeEntry::new("TYPECK_CLASS_ARITY", Self::TYPECK_CLASS_ARITY), + DiagnosticCodeEntry::new( + "TYPECK_OVERLAPPING_INSTANCE", + Self::TYPECK_OVERLAPPING_INSTANCE, + ), + DiagnosticCodeEntry::new( + "TYPECK_INVALID_DEFAULT_INSTANCE", + Self::TYPECK_INVALID_DEFAULT_INSTANCE, + ), + DiagnosticCodeEntry::new( + "TYPECK_INCOMPLETE_INSTANCE", + Self::TYPECK_INCOMPLETE_INSTANCE, + ), + DiagnosticCodeEntry::new("TYPECK_INVALID_CONVERSION", Self::TYPECK_INVALID_CONVERSION), + DiagnosticCodeEntry::new( + "TYPECK_UNSUPPORTED_STORAGE_FIELD_PROJECTION", + Self::TYPECK_UNSUPPORTED_STORAGE_FIELD_PROJECTION, + ), + DiagnosticCodeEntry::new( + "TYPECK_INVALID_VALUE_TYPE_DEFINITION", + Self::TYPECK_INVALID_VALUE_TYPE_DEFINITION, + ), + DiagnosticCodeEntry::new( + "TYPECK_UNSUPPORTED_VALUE_TYPE_STORAGE", + Self::TYPECK_UNSUPPORTED_VALUE_TYPE_STORAGE, + ), + DiagnosticCodeEntry::new( + "TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE", + Self::TYPECK_UNSUPPORTED_FIXED_ARRAY_STORAGE, + ), + DiagnosticCodeEntry::new( + "TYPECK_INCOMPLETE_SIGNATURE", + Self::TYPECK_INCOMPLETE_SIGNATURE, + ), + DiagnosticCodeEntry::new( + "TYPECK_INCOMPLETE_METHOD_SIGNATURE", + Self::TYPECK_INCOMPLETE_METHOD_SIGNATURE, + ), + DiagnosticCodeEntry::new( + "TYPECK_INVALID_INSTANCE_METHOD_SIGNATURE", + Self::TYPECK_INVALID_INSTANCE_METHOD_SIGNATURE, + ), + DiagnosticCodeEntry::new( + "TYPECK_NON_FINAL_RETURN_OR_INVALID_CONSTRUCTOR_PATTERN", + Self::TYPECK_NON_FINAL_RETURN_OR_INVALID_CONSTRUCTOR_PATTERN, + ), + DiagnosticCodeEntry::new( + "TYPECK_HIDDEN_CONSTRUCTOR_COVERAGE", + Self::TYPECK_HIDDEN_CONSTRUCTOR_COVERAGE, + ), + DiagnosticCodeEntry::new( + "TYPECK_SHORTHAND_CONSTRUCTOR", + Self::TYPECK_SHORTHAND_CONSTRUCTOR, + ), + DiagnosticCodeEntry::new( + "TYPECK_GENERIC_DERIVE_CONFLICT", + Self::TYPECK_GENERIC_DERIVE_CONFLICT, + ), + DiagnosticCodeEntry::new("TYPECK_NAMESPACE_AS_VALUE", Self::TYPECK_NAMESPACE_AS_VALUE), + DiagnosticCodeEntry::new("TYPECK_CLASS_AS_TYPE", Self::TYPECK_CLASS_AS_TYPE), + DiagnosticCodeEntry::new("TYPECK_DUPLICATE_TYPE", Self::TYPECK_DUPLICATE_TYPE), + DiagnosticCodeEntry::new( + "TYPECK_CONTRACT_SELECTOR_COLLISION", + Self::TYPECK_CONTRACT_SELECTOR_COLLISION, + ), + DiagnosticCodeEntry::new( + "TYPECK_CONTRACT_RUNTIME_MAIN_ARITY", + Self::TYPECK_CONTRACT_RUNTIME_MAIN_ARITY, + ), + DiagnosticCodeEntry::new( + "TYPECK_RUNTIME_TO_COMPTIME_PARAM", + Self::TYPECK_RUNTIME_TO_COMPTIME_PARAM, + ), + DiagnosticCodeEntry::new( + "TYPECK_COMPTIME_LET_RUNTIME", + Self::TYPECK_COMPTIME_LET_RUNTIME, + ), + DiagnosticCodeEntry::new( + "TYPECK_COMPTIME_RETURN_RUNTIME", + Self::TYPECK_COMPTIME_RETURN_RUNTIME, + ), + DiagnosticCodeEntry::new( + "TYPECK_AMBIGUOUS_INFERENCE_OR_TYPE_CONSTRUCTOR_ARITY", + Self::TYPECK_AMBIGUOUS_INFERENCE_OR_TYPE_CONSTRUCTOR_ARITY, + ), + DiagnosticCodeEntry::new( + "TYPECK_NON_EXHAUSTIVE_MATCH", + Self::TYPECK_NON_EXHAUSTIVE_MATCH, + ), + DiagnosticCodeEntry::new("EMIT_NON_EXHAUSTIVE_MATCH", Self::EMIT_NON_EXHAUSTIVE_MATCH), + DiagnosticCodeEntry::new( + "TYPECK_UNREACHABLE_MATCH_ARM", + Self::TYPECK_UNREACHABLE_MATCH_ARM, + ), + DiagnosticCodeEntry::new("EMIT_EMPTY_MATCH", Self::EMIT_EMPTY_MATCH), + DiagnosticCodeEntry::new( + "SPECIALIZE_FREE_TYPE_VARIABLE", + Self::SPECIALIZE_FREE_TYPE_VARIABLE, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_INSTANTIATION_FUEL_EXHAUSTED", + Self::SPECIALIZE_INSTANTIATION_FUEL_EXHAUSTED, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_INSTANTIATION_DEPTH_EXCEEDED", + Self::SPECIALIZE_INSTANTIATION_DEPTH_EXCEEDED, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_TYPE_SIZE_EXCEEDED", + Self::SPECIALIZE_TYPE_SIZE_EXCEEDED, + ), + DiagnosticCodeEntry::new("SPECIALIZE_MISSING_BODY", Self::SPECIALIZE_MISSING_BODY), + DiagnosticCodeEntry::new( + "SPECIALIZE_MISSING_RESOLUTION", + Self::SPECIALIZE_MISSING_RESOLUTION, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_MISSING_EVIDENCE", + Self::SPECIALIZE_MISSING_EVIDENCE, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_UNSUPPORTED_EVIDENCE", + Self::SPECIALIZE_UNSUPPORTED_EVIDENCE, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_UNRESOLVED_EXTERNAL", + Self::SPECIALIZE_UNRESOLVED_EXTERNAL, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_COMPTIME_EVALUATION_FAILED", + Self::SPECIALIZE_COMPTIME_EVALUATION_FAILED, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_COMPTIME_FUEL_EXHAUSTED", + Self::SPECIALIZE_COMPTIME_FUEL_EXHAUSTED, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_INTEGER_ERASURE", + Self::SPECIALIZE_INTEGER_ERASURE, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_PUBLIC_COMPTIME_PARAM", + Self::SPECIALIZE_PUBLIC_COMPTIME_PARAM, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_REDUCTION_RECURSION", + Self::SPECIALIZE_REDUCTION_RECURSION, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_REDUCTION_FUEL_EXHAUSTED", + Self::SPECIALIZE_REDUCTION_FUEL_EXHAUSTED, + ), + DiagnosticCodeEntry::new( + "SPECIALIZE_UNSUPPORTED_RUNTIME_TYPE", + Self::SPECIALIZE_UNSUPPORTED_RUNTIME_TYPE, + ), + DiagnosticCodeEntry::new("EMIT_UNSUPPORTED_TYPE", Self::EMIT_UNSUPPORTED_TYPE), + DiagnosticCodeEntry::new("EMIT_UNSUPPORTED_LITERAL", Self::EMIT_UNSUPPORTED_LITERAL), + DiagnosticCodeEntry::new( + "EMIT_UNSUPPORTED_MONO_CONSTRUCT", + Self::EMIT_UNSUPPORTED_MONO_CONSTRUCT, + ), + DiagnosticCodeEntry::new("EMIT_MISSING_ADT_LAYOUT", Self::EMIT_MISSING_ADT_LAYOUT), + DiagnosticCodeEntry::new("EMIT_MISSING_CONSTRUCTOR", Self::EMIT_MISSING_CONSTRUCTOR), + DiagnosticCodeEntry::new("EMIT_DISPATCHER_DEFERRED", Self::EMIT_DISPATCHER_DEFERRED), + DiagnosticCodeEntry::new( + "EMIT_UNSUPPORTED_DISPATCH_ENTRY", + Self::EMIT_UNSUPPORTED_DISPATCH_ENTRY, + ), + DiagnosticCodeEntry::new( + "EMIT_MULTI_SCRUTINEE_MATCH", + Self::EMIT_MULTI_SCRUTINEE_MATCH, + ), + DiagnosticCodeEntry::new("HULL_UNDEFINED_VARIABLE", Self::HULL_UNDEFINED_VARIABLE), + DiagnosticCodeEntry::new("HULL_UNDEFINED_FUNCTION", Self::HULL_UNDEFINED_FUNCTION), + DiagnosticCodeEntry::new("HULL_DUPLICATE_FUNCTION", Self::HULL_DUPLICATE_FUNCTION), + DiagnosticCodeEntry::new("HULL_ARITY_MISMATCH", Self::HULL_ARITY_MISMATCH), + DiagnosticCodeEntry::new("HULL_TYPE_MISMATCH", Self::HULL_TYPE_MISMATCH), + DiagnosticCodeEntry::new( + "HULL_EXPR_ANNOTATION_MISMATCH", + Self::HULL_EXPR_ANNOTATION_MISMATCH, + ), + DiagnosticCodeEntry::new("HULL_EXPECTED_PRODUCT", Self::HULL_EXPECTED_PRODUCT), + DiagnosticCodeEntry::new("HULL_EXPECTED_SUM", Self::HULL_EXPECTED_SUM), + DiagnosticCodeEntry::new("HULL_EXPECTED_BOOL", Self::HULL_EXPECTED_BOOL), + DiagnosticCodeEntry::new("HULL_BAD_INJECTION_INDEX", Self::HULL_BAD_INJECTION_INDEX), + DiagnosticCodeEntry::new("HULL_BAD_MATCH_PATTERN", Self::HULL_BAD_MATCH_PATTERN), + DiagnosticCodeEntry::new( + "HULL_RETURN_OUTSIDE_FUNCTION", + Self::HULL_RETURN_OUTSIDE_FUNCTION, + ), + DiagnosticCodeEntry::new( + "HULL_FUNCTION_TYPE_NOT_FIRST_ORDER", + Self::HULL_FUNCTION_TYPE_NOT_FIRST_ORDER, + ), + DiagnosticCodeEntry::new("HULL_MISSING_TERMINATOR", Self::HULL_MISSING_TERMINATOR), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_REQUIRES_DATABASE", + Self::HULL_ASSEMBLY_REQUIRES_DATABASE, + ), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_RETURN_COUNT_MISMATCH", + Self::HULL_ASSEMBLY_RETURN_COUNT_MISMATCH, + ), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_EXPRESSION_NOT_UNIT", + Self::HULL_ASSEMBLY_EXPRESSION_NOT_UNIT, + ), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_EXPECTED_WORD_ARGUMENT", + Self::HULL_ASSEMBLY_EXPECTED_WORD_ARGUMENT, + ), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_EXPECTED_WORD_ASSIGNMENT", + Self::HULL_ASSEMBLY_EXPECTED_WORD_ASSIGNMENT, + ), + DiagnosticCodeEntry::new( + "HULL_ASSEMBLY_VOID_ARGUMENT", + Self::HULL_ASSEMBLY_VOID_ARGUMENT, + ), + ]; + + /// Duplicate code values that are intentional compatibility aliases. + pub const INTENTIONAL_DUPLICATES: &'static [DiagnosticCodeAlias] = &[ + DiagnosticCodeAlias::new( + Self::MODULE_UNKNOWN_REEXPORT, + "SC0115 covers both missing re-exported names and missing re-exported constructors.", + ), + DiagnosticCodeAlias::new( + Self::TYPECK_WRONG_ARITY, + "SC0203 covers ordinary arity mismatches and reference-compatible mutually recursive enum errors.", + ), + DiagnosticCodeAlias::new( + Self::TYPECK_INCOMPLETE_METHOD_SIGNATURE, + "SC0221 covers incomplete method signatures and invalid impl method signatures.", + ), + DiagnosticCodeAlias::new( + Self::TYPECK_CLASS_AS_TYPE, + "SC0229 covers trait-as-type errors and generated dispatch type collisions.", + ), + DiagnosticCodeAlias::new( + Self::TYPECK_NON_EXHAUSTIVE_MATCH, + "SC0302 is shared by frontend and Hull non-exhaustive match diagnostics.", + ), + DiagnosticCodeAlias::new( + Self::TYPECK_UNREACHABLE_MATCH_ARM, + "SC0303 is shared by frontend unreachable-arm and Hull empty-match diagnostics.", + ), + ]; +} diff --git a/crates/hir/src/diag/id.rs b/crates/hir/src/diag/id.rs new file mode 100644 index 00000000..332fabb3 --- /dev/null +++ b/crates/hir/src/diag/id.rs @@ -0,0 +1,233 @@ +use super::{ + span::{LabelAnchor, LabelSpan, Offset}, + value::{AnyDiagnostic, Applicability, Diagnostic, DiagnosticLevel, LabelStyle, Suggestion}, +}; +use crate::{anchor::DefKey, input::SourceFile}; + +/// Stable identity used to deduplicate diagnostics. +/// +/// The value is computed from the diagnostic level, code, headline message, +/// labels, and quick-fix suggestions. Notes are intentionally excluded so +/// presentation-only detail does not split otherwise identical diagnostics. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DiagnosticId(u64); + +/// Deterministic edge sort key for rendered diagnostics. +/// +/// The primary start is absolute and therefore this key must only be computed +/// at output boundaries such as the CLI driver or LSP publication. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DiagnosticSortKey { + /// URL of the primary file, when the diagnostic has a source label. + pub file: Option, + /// Absolute primary start offset, when a source label exists. + pub primary_start: Option, + /// Diagnostic code, e.g. `SC0101`. + pub code: Option, + /// Human-readable headline message. + pub message: String, + /// Stable identity tie-breaker for diagnostics that share the visible edge + /// key. + pub id: DiagnosticId, +} + +/// Deterministic non-absolute sort key for cached diagnostic query values. +/// +/// This key uses the source file named by the primary label anchor plus the +/// anchor-relative start offset. It is safe inside tracked queries because it +/// does not resolve def-relative spans to absolute positions. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DiagnosticQuerySortKey { + file: Option, + relative_start: Option, + code: Option, + message: String, + id: DiagnosticId, +} + +impl Diagnostic { + /// Returns a deterministic edge sort key. + /// + /// The key resolves the primary span to an absolute start offset and must + /// only be used at the output boundary. + pub fn sort_key(&self, db: &dyn crate::Db) -> DiagnosticSortKey { + let primary = self + .primary_label() + .map(|label| label.span.resolve_to_absolute(db)); + DiagnosticSortKey { + file: primary.map(|span| span.file().url(db).to_string()), + primary_start: primary.map(|span| span.start()), + code: self.code.clone(), + message: self.message.clone(), + id: self.diagnostic_id(db), + } + } + + /// Returns this diagnostic's stable deduplication identity. + pub fn diagnostic_id(&self, db: &dyn crate::Db) -> DiagnosticId { + let mut state = FNV_OFFSET; + hash_diagnostic_level(&mut state, self.level); + hash_option_str(&mut state, self.code.as_deref()); + hash_str(&mut state, &self.message); + hash_u64(&mut state, self.labels.len() as u64); + for label in &self.labels { + hash_label_span(db, &mut state, &label.span); + hash_label_style(&mut state, label.style); + hash_option_str(&mut state, label.message.as_deref()); + } + hash_u64(&mut state, self.suggestions.len() as u64); + for suggestion in &self.suggestions { + hash_suggestion(db, &mut state, suggestion); + } + DiagnosticId(state) + } + + /// Returns a deterministic non-absolute sort key for use inside queries. + pub fn query_sort_key(&self, db: &dyn crate::Db) -> DiagnosticQuerySortKey { + let primary = self.primary_label(); + DiagnosticQuerySortKey { + file: primary.map(|label| label.span.file().url(db).to_string()), + relative_start: primary.map(|label| label.span.begin()), + code: self.code.clone(), + message: self.message.clone(), + id: self.diagnostic_id(db), + } + } +} + +impl AnyDiagnostic { + /// Returns the stable deduplication identity after lowering. + pub fn diagnostic_id(&self, db: &dyn crate::Db) -> DiagnosticId { + self.lower(db).diagnostic_id(db) + } + + /// Returns a deterministic non-absolute sort key for use inside queries. + pub fn query_sort_key(&self, db: &dyn crate::Db) -> DiagnosticQuerySortKey { + self.lower(db).query_sort_key(db) + } +} + +const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; +const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; + +fn hash_bytes(state: &mut u64, bytes: &[u8]) { + for byte in bytes { + *state ^= u64::from(*byte); + *state = state.wrapping_mul(FNV_PRIME); + } +} + +fn hash_u8(state: &mut u64, value: u8) { + hash_bytes(state, &[value]); +} + +fn hash_u32(state: &mut u64, value: u32) { + hash_bytes(state, &value.to_le_bytes()); +} + +fn hash_u64(state: &mut u64, value: u64) { + hash_bytes(state, &value.to_le_bytes()); +} + +fn hash_str(state: &mut u64, value: &str) { + hash_u64(state, value.len() as u64); + hash_bytes(state, value.as_bytes()); +} + +fn hash_option_str(state: &mut u64, value: Option<&str>) { + match value { + Some(value) => { + hash_u8(state, 1); + hash_str(state, value); + } + None => hash_u8(state, 0), + } +} + +fn hash_source_file(db: &dyn crate::Db, state: &mut u64, file: SourceFile) { + hash_str(state, file.url(db).as_str()); +} + +fn hash_label_span(db: &dyn crate::Db, state: &mut u64, span: &LabelSpan) { + match &span.anchor { + LabelAnchor::Root(file) => { + hash_u8(state, 0); + hash_source_file(db, state, *file); + } + LabelAnchor::Def(key) => { + hash_u8(state, 1); + hash_def_key(db, state, key); + } + } + hash_u32(state, span.begin.as_u32()); + hash_u32(state, span.end.as_u32()); +} + +fn hash_def_key(db: &dyn crate::Db, state: &mut u64, key: &DefKey) { + hash_source_file(db, state, key.file); + match &key.owner { + Some(owner) => { + hash_u8(state, 1); + hash_def_key(db, state, owner); + } + None => hash_u8(state, 0), + } + hash_str(state, def_kind_name(key.kind)); + hash_option_str(state, key.name.as_deref()); + hash_option_str(state, key.fingerprint.as_deref()); + hash_u32(state, key.disambiguator.as_u32()); +} + +fn hash_diagnostic_level(state: &mut u64, level: DiagnosticLevel) { + match level { + DiagnosticLevel::Error => hash_u8(state, 0), + DiagnosticLevel::Warning => hash_u8(state, 1), + DiagnosticLevel::Note => hash_u8(state, 2), + DiagnosticLevel::Help => hash_u8(state, 3), + } +} + +fn def_kind_name(kind: crate::anchor::DefKind) -> &'static str { + match kind { + crate::anchor::DefKind::Module => "module", + crate::anchor::DefKind::Function => "function", + crate::anchor::DefKind::FuncBody => "func_body", + crate::anchor::DefKind::TypeAlias => "type_alias", + crate::anchor::DefKind::ValueType => "value_type", + crate::anchor::DefKind::Adt => "adt", + crate::anchor::DefKind::AdtCtor => "adt_ctor", + crate::anchor::DefKind::Class => "trait", + crate::anchor::DefKind::Instance => "impl", + crate::anchor::DefKind::Contract => "contract", + crate::anchor::DefKind::Field => "field", + crate::anchor::DefKind::Import => "import", + crate::anchor::DefKind::Export => "export", + crate::anchor::DefKind::Pragma => "pragma", + } +} + +fn hash_label_style(state: &mut u64, style: LabelStyle) { + match style { + LabelStyle::Primary => hash_u8(state, 0), + LabelStyle::Secondary => hash_u8(state, 1), + } +} + +fn hash_suggestion(db: &dyn crate::Db, state: &mut u64, suggestion: &Suggestion) { + hash_str(state, &suggestion.title); + hash_applicability(state, suggestion.applicability); + hash_u64(state, suggestion.edits.len() as u64); + for edit in &suggestion.edits { + hash_label_span(db, state, &edit.span); + hash_str(state, &edit.replacement); + } +} + +fn hash_applicability(state: &mut u64, applicability: Applicability) { + match applicability { + Applicability::MachineApplicable => hash_u8(state, 0), + Applicability::MaybeIncorrect => hash_u8(state, 1), + Applicability::HasPlaceholders => hash_u8(state, 2), + Applicability::Unspecified => hash_u8(state, 3), + } +} diff --git a/crates/hir/src/diag/mod.rs b/crates/hir/src/diag/mod.rs new file mode 100644 index 00000000..8b89a71b --- /dev/null +++ b/crates/hir/src/diag/mod.rs @@ -0,0 +1,30 @@ +//! Diagnostic values and source rendering. +//! +//! Diagnostics outlive the tracked query stack that creates them, so labels +//! cannot store a `Span<'db>` directly. Instead each label snapshots the span +//! into a lifetime-free `LabelSpan`: root anchors keep their `SourceFile`, +//! and def anchors keep a structural `DefKey`. Rendering rehydrates that key +//! against the current database and resolves it through the def-location table. +//! +//! This preserves the anchor-relative design while making diagnostics portable +//! as ordinary query values. Label resolution follows the same edge-only rule +//! as other absolute span work: diagnostics are resolved when they are rendered +//! or sorted for publication, not while semantic results are cached. + +mod code; +mod id; +mod render; +mod sort; +mod span; +#[cfg(test)] +mod tests; +mod value; + +pub use code::{DiagnosticCode, DiagnosticCodeAlias, DiagnosticCodeEntry}; +pub use id::{DiagnosticId, DiagnosticQuerySortKey, DiagnosticSortKey}; +pub use sort::{sort_dedup_query_diagnostics, sort_dedup_rendered_diagnostics}; +pub use span::{AbsoluteSpan, LabelSpan, Offset}; +pub use value::{ + AnchoredTextEdit, AnyDiagnostic, Applicability, Diagnostic, DiagnosticLabel, DiagnosticLevel, + LabelStyle, Suggestion, +}; diff --git a/crates/hir/src/diag/render.rs b/crates/hir/src/diag/render.rs new file mode 100644 index 00000000..ddf69a1a --- /dev/null +++ b/crates/hir/src/diag/render.rs @@ -0,0 +1,333 @@ +use annotate_snippets::{Annotation, AnnotationKind, Group, Level, Renderer, Snippet}; + +use super::{ + span::AbsoluteSpan, + value::{Diagnostic, DiagnosticLabel, DiagnosticLevel, LabelStyle}, +}; +use crate::input::SourceFile; + +impl Diagnostic { + /// Converts this diagnostic into `annotate_snippets` groups. + /// + /// This is where label spans are resolved to absolute file offsets. Labels + /// whose files have no available content are skipped, but notes still + /// render. + pub fn to_annotate_report<'db>(&self, db: &'db dyn crate::Db) -> Vec> { + let mut title = self + .level + .to_annotate_level() + .primary_title(self.message.clone()); + if let Some(code) = &self.code { + title = title.id(code.clone()); + } + + let mut group = Group::with_title(title); + + let mut by_file: Vec<(SourceFile, Vec<(&DiagnosticLabel, AbsoluteSpan)>)> = Vec::new(); + for label in &self.labels { + if label.span.file().content(db).is_none() { + continue; + } + let absolute = label.span.resolve_to_absolute(db); + let file = absolute.file(); + if let Some((_, labels)) = by_file + .iter_mut() + .find(|(existing_file, _)| *existing_file == file) + { + labels.push((label, absolute)); + } else { + by_file.push((file, vec![(label, absolute)])); + } + } + + for (file, labels) in by_file { + let url = file.url(db); + let Some(content) = file.content(db) else { + continue; + }; + + let source_len = content.len(); + let mut annotations: Vec> = Vec::with_capacity(labels.len()); + let mut visible_ranges = Vec::with_capacity(labels.len()); + + for (label, absolute) in labels { + let span = clamp_span( + absolute.start().as_usize(), + absolute.end().as_usize(), + source_len, + ); + visible_ranges.push(context_window_span(content.as_str(), &span, 1, 1)); + let mut annotation = label.style.to_annotate_kind().span(span); + if let Some(message) = &label.message { + annotation = annotation.label(message.clone()); + } + if matches!(label.style, LabelStyle::Primary) { + annotation = annotation.highlight_source(true); + } + annotations.push(annotation); + } + + let mut snippet = Snippet::source(content).path(display_url_path(url)); + for range in merge_ranges(visible_ranges) { + snippet = snippet.annotation(AnnotationKind::Visible.span(range)); + } + snippet = snippet.annotations(annotations); + + group = group.element(snippet); + } + + for note in &self.notes { + group = group.element(Level::NOTE.message(note.clone())); + } + for help in &self.helps { + group = group.element(Level::HELP.message(help.clone())); + } + + vec![group] + } + + /// Renders this diagnostic using the default styled terminal renderer. + pub fn render(&self, db: &dyn crate::Db) -> String { + self.render_with(db, &Renderer::styled()) + } + + /// Renders this diagnostic using the provided `annotate_snippets` renderer. + /// + /// This performs absolute span resolution for labels whose files still have + /// content, and may panic if such a def-relative label no longer has a + /// location table entry. + pub fn render_with(&self, db: &dyn crate::Db, renderer: &Renderer) -> String { + let report = self.to_annotate_report(db); + renderer.render(&report) + } + + /// Renders this diagnostic as a single line: + /// `path:line:column: error[CODE]: message`. + /// + /// Multi-line messages are compacted so short output remains one diagnostic + /// per line. + pub fn render_short(&self, db: &dyn crate::Db) -> String { + let mut output = String::new(); + if let Some(label) = self.primary_label() { + let absolute = label.span.resolve_to_absolute(db); + let file = absolute.file(); + let path = display_url_path(file.url(db)); + if let Some(content) = file.content(db) { + let (line, column) = line_column_for_offset(content, absolute.start().as_usize()); + output.push_str(&format!("{path}:{line}:{column}: ")); + } else { + output.push_str(&format!("{path}: ")); + } + } + output.push_str(self.level.as_str()); + if let Some(code) = &self.code { + output.push('['); + output.push_str(code); + output.push(']'); + } + output.push_str(": "); + output.push_str(&compact_diagnostic_message(&self.message)); + output.push('\n'); + output + } +} + +fn display_url_path(url: &url::Url) -> String { + // `Url::to_file_path` accepts hostless custom schemes on Unix, but Windows + // rejects their drive-less paths. Decode compiler-owned virtual URLs + // explicitly so diagnostic output stays platform-independent. + if url.scheme() == "memory" + && url.host_str().is_none() + && url.path().starts_with('/') + && let Ok(path) = percent_encoding::percent_decode_str(url.path()).decode_utf8() + { + return path.into_owned(); + } + + crate::url_to_file_path(url) + .map(|path| path.display().to_string()) + .unwrap_or_else(|| url.as_str().to_owned()) +} + +impl DiagnosticLevel { + fn to_annotate_level(self) -> Level<'static> { + match self { + DiagnosticLevel::Error => Level::ERROR, + DiagnosticLevel::Warning => Level::WARNING, + DiagnosticLevel::Note => Level::NOTE, + DiagnosticLevel::Help => Level::HELP, + } + } + + fn as_str(self) -> &'static str { + match self { + DiagnosticLevel::Error => "error", + DiagnosticLevel::Warning => "warning", + DiagnosticLevel::Note => "note", + DiagnosticLevel::Help => "help", + } + } +} + +impl LabelStyle { + fn to_annotate_kind(self) -> AnnotationKind { + match self { + LabelStyle::Primary => AnnotationKind::Primary, + LabelStyle::Secondary => AnnotationKind::Context, + } + } +} + +fn clamp_span(start: usize, end: usize, source_len: usize) -> core::ops::Range { + let start = start.min(source_len); + let end = end.min(source_len); + if start <= end { start..end } else { end..start } +} + +fn context_window_span( + source: &str, + focus: &core::ops::Range, + lines_before: usize, + lines_after: usize, +) -> core::ops::Range { + if source.is_empty() { + return 0..0; + } + + let focus_start = normalize_line_lookup_offset(source, focus.start); + let focus_end = normalize_line_lookup_offset(source, focus.end); + + let mut start = line_start_at_or_before(source, focus_start); + for _ in 0..lines_before { + if start == 0 { + break; + } + start = line_start_at_or_before(source, start.saturating_sub(1)); + } + + let mut end = line_end_at_or_after(source, focus_end); + for _ in 0..lines_after { + if end >= source.len() { + break; + } + end = line_end_at_or_after(source, (end + 1).min(source.len())); + } + + let target_lines = lines_before + lines_after + 1; + while count_lines_in_span(source, start, end) < target_lines { + if start > 0 { + start = line_start_at_or_before(source, start.saturating_sub(1)); + continue; + } + if end < source.len() { + end = line_end_at_or_after(source, (end + 1).min(source.len())); + } else { + break; + } + } + + if start == end && !source.is_empty() { + start..(end + 1).min(source.len()) + } else { + start..end + } +} + +fn normalize_line_lookup_offset(source: &str, offset: usize) -> usize { + let mut offset = offset.min(source.len()); + if offset == source.len() { + offset = floor_char_boundary(source, offset.saturating_sub(1)); + } + let bytes = source.as_bytes(); + if bytes.get(offset).copied() == Some(b'\n') && offset > 0 { + offset = floor_char_boundary(source, offset - 1); + } + offset +} + +fn floor_char_boundary(source: &str, offset: usize) -> usize { + let mut offset = offset.min(source.len()); + while offset > 0 && !source.is_char_boundary(offset) { + offset -= 1; + } + offset +} + +fn ceil_char_boundary(source: &str, offset: usize) -> usize { + let mut offset = offset.min(source.len()); + while offset < source.len() && !source.is_char_boundary(offset) { + offset += 1; + } + offset +} + +fn line_start_at_or_before(source: &str, offset: usize) -> usize { + let offset = floor_char_boundary(source, offset); + source[..offset].rfind('\n').map_or(0, |idx| idx + 1) +} + +fn line_end_at_or_after(source: &str, offset: usize) -> usize { + let offset = ceil_char_boundary(source, offset); + source[offset..] + .find('\n') + .map_or(source.len(), |idx| offset + idx) +} + +fn merge_ranges(mut ranges: Vec>) -> Vec> { + if ranges.len() <= 1 { + return ranges; + } + + ranges.sort_by_key(|range| (range.start, range.end)); + let mut merged: Vec> = Vec::with_capacity(ranges.len()); + for range in ranges { + if let Some(last) = merged.last_mut() { + if range.start <= last.end { + if range.end > last.end { + last.end = range.end; + } + } else { + merged.push(range); + } + } else { + merged.push(range); + } + } + merged +} + +fn count_lines_in_span(source: &str, start: usize, end: usize) -> usize { + if source.is_empty() { + return 0; + } + let start = start.min(source.len()); + let end = end.min(source.len()); + if start >= end { + return 1; + } + let mut count = source[start..end] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1; + if end == source.len() && source.ends_with('\n') && count > 0 { + count -= 1; + } + count +} + +fn line_column_for_offset(source: &str, offset: usize) -> (usize, usize) { + let offset = floor_char_boundary(source, offset.min(source.len())); + let line = source[..offset] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1; + let line_start = line_start_at_or_before(source, offset); + let column = source[line_start..offset].chars().count() + 1; + (line, column) +} + +fn compact_diagnostic_message(message: &str) -> String { + message.split_whitespace().collect::>().join(" ") +} diff --git a/crates/hir/src/diag/sort.rs b/crates/hir/src/diag/sort.rs new file mode 100644 index 00000000..0e87c3f6 --- /dev/null +++ b/crates/hir/src/diag/sort.rs @@ -0,0 +1,23 @@ +use rustc_hash::FxHashSet; + +use super::{AnyDiagnostic, Diagnostic, DiagnosticId}; + +/// Sorts and deduplicates diagnostics returned from tracked diagnostic queries. +/// +/// This uses anchor-relative query keys and does not resolve def-relative spans +/// to absolute file offsets, so it is safe inside Salsa-tracked code. +pub fn sort_dedup_query_diagnostics(db: &dyn crate::Db, diagnostics: &mut Vec) { + diagnostics.sort_by_key(|diagnostic| diagnostic.query_sort_key(db)); + let mut seen = FxHashSet::::default(); + diagnostics.retain(|diagnostic| seen.insert(diagnostic.diagnostic_id(db))); +} + +/// Sorts and deduplicates already-renderable diagnostics at an output edge. +/// +/// This uses absolute primary-label positions and must only be called outside +/// tracked query results, such as by the CLI driver or tests after lowering. +pub fn sort_dedup_rendered_diagnostics(db: &dyn crate::Db, diagnostics: &mut Vec) { + diagnostics.sort_by_key(|diagnostic| diagnostic.sort_key(db)); + let mut seen = FxHashSet::::default(); + diagnostics.retain(|diagnostic| seen.insert(diagnostic.diagnostic_id(db))); +} diff --git a/crates/hir/src/diag/span.rs b/crates/hir/src/diag/span.rs new file mode 100644 index 00000000..5b8984c4 --- /dev/null +++ b/crates/hir/src/diag/span.rs @@ -0,0 +1,173 @@ +use crate::{ + anchor::{DefId, DefKey, resolve_def_location_or_bug}, + input::SourceFile, + span::{AnchorKind, Span}, +}; + +/// Lifetime-free anchor used by diagnostics. +/// +/// This mirrors `AnchorKind<'db>` without storing database-lifetime values. +/// Def anchors are stored as structural keys so they can be interned again when +/// a diagnostic is rendered. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub(super) enum LabelAnchor { + Root(SourceFile), + Def(DefKey), +} + +/// Lifetime-free span snapshot stored in diagnostics. +/// +/// The snapshot keeps relative offsets and enough anchor identity to resolve +/// later. It intentionally avoids absolute offsets so byte-shift invariance is +/// preserved until rendering. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct LabelSpan { + pub(super) anchor: LabelAnchor, + pub(super) begin: Offset, + pub(super) end: Offset, +} + +impl LabelSpan { + pub(super) fn new(anchor: LabelAnchor, begin: Offset, end: Offset) -> Self { + assert!(begin <= end, "span start must be <= end"); + Self { anchor, begin, end } + } + + /// Snapshots a HIR span into a lifetime-free diagnostic span. + /// + /// The snapshot keeps only anchor-relative offsets. Absolute file offsets + /// are still resolved later at diagnostic/LSP boundaries. + pub fn from_span<'db>(db: &'db dyn crate::Db, span: Span<'db>) -> Self { + let anchor = match span.anchor().kind_value(db) { + AnchorKind::Root(file) => LabelAnchor::Root(file), + AnchorKind::Def(def) => LabelAnchor::Def(def.key(db)), + }; + Self::new(anchor, span.begin(), span.end()) + } + + /// Returns the source file named by this span's anchor. + pub fn file(&self) -> SourceFile { + match &self.anchor { + LabelAnchor::Root(file) => *file, + LabelAnchor::Def(key) => key.file, + } + } + + /// Returns the anchor-relative start offset. + pub const fn begin(&self) -> Offset { + self.begin + } + + /// Returns the anchor-relative end offset. + pub const fn end(&self) -> Offset { + self.end + } + + /// Resolves this span to absolute offsets. + /// + /// This is an edge-only operation. Do not call it inside tracked semantic + /// queries because it consults the current def-location table. + pub fn resolve_to_absolute(&self, db: &dyn crate::Db) -> AbsoluteSpan { + let (file, base) = match &self.anchor { + LabelAnchor::Root(file) => (*file, Offset::new(0)), + LabelAnchor::Def(key) => { + let table = db.def_location_table(key.file); + let def = DefId::from_key(db, key); + let loc = resolve_def_location_or_bug(table, def, "def key", key); + (loc.file, loc.base_offset) + } + }; + AbsoluteSpan::new( + file, + Offset::checked_add_or_bug(base, self.begin, "resolving diagnostic span"), + Offset::checked_add_or_bug(base, self.end, "resolving diagnostic span"), + ) + } +} + +/// Byte offset into a source file. +/// +/// Offsets are byte-based, not character-based. The `u32` storage keeps span +/// values compact inside HIR and diagnostics; conversion from larger indices is +/// fallible through [`Offset::try_from_usize`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, salsa::Update)] +pub struct Offset(u32); + +impl Offset { + /// Creates an offset from a raw `u32` byte index. + pub const fn new(raw: u32) -> Self { + Self(raw) + } + + /// Returns this offset as a `u32` byte index. + pub const fn as_u32(self) -> u32 { + self.0 + } + + /// Returns this offset as a `usize` byte index. + pub fn as_usize(self) -> usize { + self.0 as usize + } + + /// Tries to create an offset from `usize`. + pub fn try_from_usize(raw: usize) -> Option { + u32::try_from(raw).ok().map(Self) + } + + pub(crate) fn checked_add_or_bug(base: Self, rel: Self, context: &'static str) -> Self { + let Some(raw) = base.as_u32().checked_add(rel.as_u32()) else { + panic!("offset overflow while {}", context); + }; + Self::new(raw) + } +} + +/// Span represented as absolute offsets in a specific file. +/// +/// This type is used only after an anchor-relative span has crossed an output +/// boundary. Semantic queries should generally carry [`Span`] +/// instead. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct AbsoluteSpan { + /// File containing the absolute byte range. + pub file: SourceFile, + /// Inclusive start byte offset. + pub start: Offset, + /// Exclusive end byte offset. + pub end: Offset, +} + +impl AbsoluteSpan { + /// Creates a new absolute span. + /// + /// Panics if `start > end`. + pub fn new(file: SourceFile, start: Offset, end: Offset) -> Self { + assert!(start <= end, "span start must be <= end"); + Self { file, start, end } + } + + /// Returns the file this span belongs to. + pub const fn file(self) -> SourceFile { + self.file + } + + /// Returns the start byte offset. + pub const fn start(self) -> Offset { + self.start + } + + /// Returns the end byte offset. + pub const fn end(self) -> Offset { + self.end + } + + /// Returns span length in bytes. + pub fn len(self) -> u32 { + self.end.as_u32() - self.start.as_u32() + } + + /// Returns `true` when the span is empty. + pub fn is_empty(self) -> bool { + self.start == self.end + } +} diff --git a/crates/hir/src/diag/tests.rs b/crates/hir/src/diag/tests.rs new file mode 100644 index 00000000..27336c50 --- /dev/null +++ b/crates/hir/src/diag/tests.rs @@ -0,0 +1,279 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use annotate_snippets::Renderer; + +use super::{span::LabelAnchor, *}; +use crate::{ + anchor::{DefId, DefKind, DefLocationTable, Disambiguator}, + input::SourceFile, + nameres::{NameresDiagnostic, UndefinedNameKind}, +}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::tracked(returns(ref))] +fn empty_def_location_table<'db>( + db: &'db dyn crate::Db, + file: SourceFile, +) -> DefLocationTable<'db> { + let _ = (db, file); + DefLocationTable::default() +} + +#[salsa::db] +impl crate::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + empty_def_location_table(self, file) + } +} + +fn source_file(db: &TestDb, name: &str, content: Option<&str>) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, content.map(ToOwned::to_owned)) +} + +fn file_source_file(db: &TestDb, path: &str, content: &str) -> SourceFile { + let url = url::Url::from_file_path(path).expect("valid absolute file path"); + SourceFile::new(db, url, Some(content.to_owned())) +} + +fn root_span(file: SourceFile, start: u32, end: u32) -> LabelSpan { + LabelSpan::new( + LabelAnchor::Root(file), + Offset::new(start), + Offset::new(end), + ) +} + +#[test] +fn diagnostic_id_includes_level_and_suggestions() { + let db = TestDb::default(); + let file = source_file(&db, "ids", Some("let x = 1;\n")); + let primary = root_span(file, 0, 3); + let edit = root_span(file, 4, 5); + + let error = Diagnostic::error("same headline") + .with_code("SC9999") + .with_primary_label_span(primary.clone(), Some("same label")); + let warning = Diagnostic::warning("same headline") + .with_code("SC9999") + .with_primary_label_span(primary.clone(), Some("same label")); + + assert_ne!(error.diagnostic_id(&db), warning.diagnostic_id(&db)); + + let with_machine_fix = error.clone().with_suggestion(Suggestion { + title: "rename".to_owned(), + applicability: Applicability::MachineApplicable, + edits: vec![AnchoredTextEdit { + span: edit.clone(), + replacement: "y".to_owned(), + }], + }); + let with_review_fix = error.with_suggestion(Suggestion { + title: "rename".to_owned(), + applicability: Applicability::MaybeIncorrect, + edits: vec![AnchoredTextEdit { + span: edit, + replacement: "z".to_owned(), + }], + }); + + assert_ne!( + with_machine_fix.diagnostic_id(&db), + with_review_fix.diagnostic_id(&db) + ); +} + +#[test] +fn undefined_name_kind_stays_off_the_rendering_surface() { + let db = TestDb::default(); + let file = source_file(&db, "undefined", Some("missing\n")); + let span = root_span(file, 0, 7); + let lowered = |kind| { + NameresDiagnostic::UndefinedName { + name: "missing".to_owned(), + span: span.clone(), + kind, + suggestion: None, + private_candidate: None, + } + .lower(&db) + }; + + let term = lowered(UndefinedNameKind::Term); + for kind in [ + UndefinedNameKind::ModuleQualifier { + access_path: "missing.member".to_owned(), + }, + UndefinedNameKind::QualifiedConstructor { + access_path: "missing.Constructor".to_owned(), + }, + UndefinedNameKind::ModuleMember { + access_path: "missing.member".to_owned(), + }, + UndefinedNameKind::Field, + UndefinedNameKind::Other, + ] { + let diagnostic = lowered(kind); + assert_eq!(diagnostic, term); + assert_eq!(diagnostic.diagnostic_id(&db), term.diagnostic_id(&db)); + } +} + +#[test] +fn diagnostic_sort_key_uses_diagnostic_id_tiebreaker() { + let db = TestDb::default(); + let file = source_file(&db, "sort", Some("alpha beta gamma\n")); + let primary = root_span(file, 0, 5); + + let first = Diagnostic::error("same headline") + .with_code("SC9999") + .with_primary_label_span(primary.clone(), None::) + .with_secondary_label_span(root_span(file, 6, 10), Some("first secondary")); + let second = Diagnostic::error("same headline") + .with_code("SC9999") + .with_primary_label_span(primary, None::) + .with_secondary_label_span(root_span(file, 11, 16), Some("second secondary")); + + let first_key = first.sort_key(&db); + let second_key = second.sort_key(&db); + assert_eq!(first_key.file, second_key.file); + assert_eq!(first_key.primary_start, second_key.primary_start); + assert_eq!(first_key.code, second_key.code); + assert_eq!(first_key.message, second_key.message); + assert_ne!(first_key.id, second_key.id); + assert_ne!(first_key, second_key); + + let mut original_order = [first.clone(), second.clone()]; + original_order.sort_by_key(|diagnostic| diagnostic.sort_key(&db)); + let mut reversed_order = [second, first]; + reversed_order.sort_by_key(|diagnostic| diagnostic.sort_key(&db)); + + let original_ids = original_order + .iter() + .map(|diagnostic| diagnostic.diagnostic_id(&db)) + .collect::>(); + let reversed_ids = reversed_order + .iter() + .map(|diagnostic| diagnostic.diagnostic_id(&db)) + .collect::>(); + assert_eq!(original_ids, reversed_ids); +} + +#[test] +fn diagnostic_code_registry_has_only_documented_aliases() { + let mut by_code = BTreeMap::<&str, Vec<&str>>::new(); + for entry in DiagnosticCode::ALL { + by_code.entry(entry.code()).or_default().push(entry.name()); + } + + let mut allowed = BTreeMap::<&str, &str>::new(); + for alias in DiagnosticCode::INTENTIONAL_DUPLICATES { + assert!( + !alias.reason().trim().is_empty(), + "intentional duplicate {} needs a reason", + alias.code() + ); + assert!( + allowed.insert(alias.code(), alias.reason()).is_none(), + "duplicate allow-list entry for {}", + alias.code() + ); + } + + let mut undocumented = Vec::new(); + for (code, names) in &by_code { + if names.len() > 1 && !allowed.contains_key(code) { + undocumented.push(format!("{code}: {}", names.join(", "))); + } + } + assert!( + undocumented.is_empty(), + "duplicate diagnostic codes need explicit allow-list entries: {}", + undocumented.join("; ") + ); + + let duplicate_codes = by_code + .iter() + .filter_map(|(code, names)| (names.len() > 1).then_some(*code)) + .collect::>(); + for code in allowed.keys() { + assert!( + duplicate_codes.contains(*code), + "allow-list entry {code} does not correspond to duplicate registry values" + ); + } +} + +#[test] +fn render_skips_contentless_def_labels_before_absolute_resolution() { + let db = TestDb::default(); + let file = source_file(&db, "missing", None); + let def = DefId::new( + &db, + file, + None, + DefKind::Function, + Some("f".to_owned()), + None, + Disambiguator::ZERO, + ); + let stale_def_span = LabelSpan::new( + LabelAnchor::Def(def.key(&db)), + Offset::new(0), + Offset::new(1), + ); + let diagnostic = Diagnostic::error("stale diagnostic") + .with_code("SC9998") + .with_primary_label_span(stale_def_span, Some("stale label")) + .with_note("note still renders"); + + let rendered = diagnostic.render_with(&db, &Renderer::plain()); + assert!(rendered.contains("stale diagnostic")); + assert!(rendered.contains("note still renders")); + assert!(!rendered.contains("stale label")); +} + +#[test] +fn render_decodes_file_urls_in_human_and_short_formats() { + let db = TestDb::default(); + let file = file_source_file(&db, "/tmp/Solcore Project/日本語/main.solc", "missing\n"); + let diagnostic = Diagnostic::error("undefined name") + .with_primary_label_span(root_span(file, 0, 7), Some("not found")); + + let human = diagnostic.render_with(&db, &Renderer::plain()); + let short = diagnostic.render_short(&db); + + for rendered in [human, short] { + assert!(rendered.contains("/tmp/Solcore Project/日本語/main.solc")); + assert!(!rendered.contains("%20")); + assert!(!rendered.contains("%E6")); + } +} + +#[test] +fn render_decodes_memory_urls_in_human_and_short_formats() { + let db = TestDb::default(); + let url = url::Url::parse("memory:///Solcore%20Project/%E6%97%A5%E6%9C%AC%E8%AA%9E/main.solc") + .expect("valid memory URL"); + let file = SourceFile::new(&db, url, Some("missing\n".to_owned())); + let diagnostic = Diagnostic::error("undefined name") + .with_primary_label_span(root_span(file, 0, 7), Some("not found")); + + let human = diagnostic.render_with(&db, &Renderer::plain()); + let short = diagnostic.render_short(&db); + + for rendered in [human, short] { + assert!(rendered.contains("/Solcore Project/日本語/main.solc")); + assert!(!rendered.contains("memory:///")); + assert!(!rendered.contains("%20")); + assert!(!rendered.contains("%E6")); + } +} diff --git a/crates/hir/src/diag/value.rs b/crates/hir/src/diag/value.rs new file mode 100644 index 00000000..2f240fd8 --- /dev/null +++ b/crates/hir/src/diag/value.rs @@ -0,0 +1,295 @@ +use super::span::LabelSpan; +use crate::{input::SourceFile, span::Span}; + +/// A diagnostic emitted during compilation. +/// +/// Diagnostics are value objects returned by pull-style diagnostic queries. +/// Their labels are stored in a lifetime-free representation so callers can +/// render them after the producing query has returned. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct Diagnostic { + /// Severity of this diagnostic. + pub level: DiagnosticLevel, + /// Human-readable headline message. + pub message: String, + /// Optional diagnostic code, e.g. `E0001`. + pub code: Option, + /// Source labels to render with this diagnostic. + pub labels: Vec, + /// Additional note text shown below the main message. + pub notes: Vec, + /// Additional help text shown below the main message. + pub helps: Vec, + /// Reserved quick-fix suggestions attached to this diagnostic. + pub suggestions: Vec, +} + +/// A diagnostic from any compiler layer before final rendering. +/// +/// Parser diagnostics are already produced as generic user-facing diagnostics. +/// HIR name-resolution diagnostics stay typed until they cross the rendering +/// boundary. Inter-module diagnostics are kept typed inside `solcore-nameres` +/// and wrapped here after lowering to the generic diagnostic surface. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum AnyDiagnostic { + /// Parser/lowering diagnostic. + Parse(Diagnostic), + /// HIR local name-resolution diagnostic. + Nameres(crate::nameres::NameresDiagnostic), + /// Type-checking diagnostic lowered at the type-checking crate edge. + Typeck(Diagnostic), + /// Inter-module loader/import/export diagnostic lowered at the crate edge. + Module(Diagnostic), +} + +/// A source edit anchored to the same lifetime-free span model as labels. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct AnchoredTextEdit { + /// Span to replace. + pub span: LabelSpan, + /// Replacement text. + pub replacement: String, +} + +/// Confidence level for applying a suggestion automatically. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum Applicability { + /// The edit can be applied mechanically. + MachineApplicable, + /// The edit is plausible but may need user review. + MaybeIncorrect, + /// The edit contains placeholders the user must fill in. + HasPlaceholders, + /// Applicability has not been classified yet. + Unspecified, +} + +/// Reserved quick-fix surface attached to user-facing diagnostics. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct Suggestion { + /// User-facing command title. + pub title: String, + /// Whether the edit can be applied automatically. + pub applicability: Applicability, + /// Text edits that implement the suggestion. + pub edits: Vec, +} + +/// Severity level for diagnostics. +/// +/// The level determines both the headline styling and how renderers categorize +/// the message. Notes and help may also appear as secondary lines on an error. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum DiagnosticLevel { + /// A compilation-blocking error. + Error, + /// A recoverable issue that should be reported to the user. + Warning, + /// Informational context. + Note, + /// Suggested remediation or explanatory help. + Help, +} + +/// Span label attached to a diagnostic. +/// +/// Labels keep their span private so construction always goes through helpers +/// that snapshot HIR spans correctly. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct DiagnosticLabel { + /// Where this label points to in source. + pub(super) span: LabelSpan, + /// Optional message displayed for this label. + pub(super) message: Option, + /// Label style used by renderers (primary/secondary). + pub(super) style: LabelStyle, +} + +/// Style of a diagnostic label. +/// +/// Primary labels highlight the main source range; secondary labels provide +/// related context such as a previous declaration. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum LabelStyle { + /// Main source location for the diagnostic. + Primary, + /// Supporting source location. + Secondary, +} + +impl Diagnostic { + /// Creates a new diagnostic with the given severity and headline message. + /// + /// The diagnostic starts without labels, notes, or code. Builders consume + /// and return `self` so query code can construct diagnostics inline before + /// accumulation. + pub fn new(level: DiagnosticLevel, message: impl Into) -> Self { + Self { + level, + message: message.into(), + code: None, + labels: Vec::new(), + notes: Vec::new(), + helps: Vec::new(), + suggestions: Vec::new(), + } + } + + /// Creates a compilation-blocking error diagnostic. + pub fn error(message: impl Into) -> Self { + Self::new(DiagnosticLevel::Error, message) + } + + /// Creates a warning diagnostic. + pub fn warning(message: impl Into) -> Self { + Self::new(DiagnosticLevel::Warning, message) + } + + /// Creates an informational diagnostic. + pub fn note(message: impl Into) -> Self { + Self::new(DiagnosticLevel::Note, message) + } + + /// Creates a help diagnostic. + pub fn help(message: impl Into) -> Self { + Self::new(DiagnosticLevel::Help, message) + } + + /// Adds a diagnostic code such as `SC0101`. + pub fn with_code(mut self, code: impl Into) -> Self { + self.code = Some(code.into()); + self + } + + /// Appends an already-snapshotted label. + pub fn with_label(mut self, label: DiagnosticLabel) -> Self { + self.labels.push(label); + self + } + + /// Appends a primary label. + pub fn with_primary_label_span( + self, + span: LabelSpan, + message: Option>, + ) -> Self { + self.with_label(DiagnosticLabel::primary(span, message)) + } + + /// Appends a primary label from a HIR span. + /// + /// The span is snapshotted immediately into a lifetime-free representation; + /// absolute file offsets are still resolved only when the diagnostic is + /// rendered. + pub fn with_primary_label<'db>( + self, + db: &'db dyn crate::Db, + span: Span<'db>, + message: Option>, + ) -> Self { + self.with_primary_label_span(LabelSpan::from_span(db, span), message) + } + + /// Appends a secondary label. + pub fn with_secondary_label_span( + self, + span: LabelSpan, + message: Option>, + ) -> Self { + self.with_label(DiagnosticLabel::secondary(span, message)) + } + + /// Appends a secondary label from a HIR span. + /// + /// Use this for related locations such as the first declaration in a + /// duplicate-definition diagnostic. + pub fn with_secondary_label<'db>( + self, + db: &'db dyn crate::Db, + span: Span<'db>, + message: Option>, + ) -> Self { + self.with_secondary_label_span(LabelSpan::from_span(db, span), message) + } + + /// Appends a note text line below the rendered source snippets. + pub fn with_note(mut self, note: impl Into) -> Self { + self.notes.push(note.into()); + self + } + + /// Appends a help text line below the rendered source snippets. + pub fn with_help(mut self, help: impl Into) -> Self { + self.helps.push(help.into()); + self + } + + /// Appends a quick-fix suggestion. + pub fn with_suggestion(mut self, suggestion: Suggestion) -> Self { + self.suggestions.push(suggestion); + self + } + + /// Returns the source file of the primary label, if any. + /// + /// This does not resolve def-relative offsets; it only reads the file + /// stored in the label anchor. + pub fn primary_file(&self, _db: &dyn crate::Db) -> Option { + self.primary_label().map(|label| label.span.file()) + } + + pub(super) fn primary_label(&self) -> Option<&DiagnosticLabel> { + self.labels + .iter() + .find(|label| matches!(label.style, LabelStyle::Primary)) + .or_else(|| self.labels.first()) + } +} + +impl AnyDiagnostic { + /// Lowers this typed or generic diagnostic to the user-facing diagnostic. + pub fn lower(&self, db: &dyn crate::Db) -> Diagnostic { + match self { + AnyDiagnostic::Parse(diagnostic) + | AnyDiagnostic::Typeck(diagnostic) + | AnyDiagnostic::Module(diagnostic) => diagnostic.clone(), + AnyDiagnostic::Nameres(diagnostic) => diagnostic.lower(db), + } + } +} + +impl DiagnosticLabel { + /// Returns this label's source span. + pub fn span(&self) -> &LabelSpan { + &self.span + } + + /// Returns this label's optional message. + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } + + /// Returns whether this label is the primary diagnostic label. + pub fn is_primary(&self) -> bool { + matches!(self.style, LabelStyle::Primary) + } + + /// Creates a new diagnostic label. + fn new(span: LabelSpan, style: LabelStyle, message: Option>) -> Self { + Self { + span, + style, + message: message.map(Into::into), + } + } + + /// Creates a primary label. + fn primary(span: LabelSpan, message: Option>) -> Self { + Self::new(span, LabelStyle::Primary, message) + } + + /// Creates a secondary label. + fn secondary(span: LabelSpan, message: Option>) -> Self { + Self::new(span, LabelStyle::Secondary, message) + } +} diff --git a/crates/hir/src/input.rs b/crates/hir/src/input.rs index 21bee212..3a5edd3e 100644 --- a/crates/hir/src/input.rs +++ b/crates/hir/src/input.rs @@ -1,8 +1,16 @@ +//! Salsa inputs that define compiler source text. +//! +//! These inputs are the mutable boundary of the compiler database. Source files +//! are identified by URL so diagnostics can render stable paths and non-file +//! sources can be represented later. + use url::Url; /// Root input for a compilation session. /// -/// It stores the full set of source files to compile together. +/// It stores the source files that are compiled together. Multi-module name +/// resolution currently uses its own module tree, but this root remains the +/// natural input for whole-program sessions and future batch queries. #[salsa::input] pub struct CompilationRoot { /// Source files that belong to this compilation unit. @@ -12,7 +20,9 @@ pub struct CompilationRoot { /// A single source file input. /// /// The file is identified by `url`, and may optionally carry in-memory -/// `content`. +/// `content`. Missing content is allowed so diagnostics and module graphs can +/// still mention a file that could not be read, but parsers treat it as empty +/// source text. #[salsa::input(debug)] pub struct SourceFile { /// Location of the source file. diff --git a/crates/hir/src/keccak.rs b/crates/hir/src/keccak.rs new file mode 100644 index 00000000..d562c70b --- /dev/null +++ b/crates/hir/src/keccak.rs @@ -0,0 +1,127 @@ +//! Minimal Keccak-256 implementation for Ethereum selectors and comptime folds. + +const ROUNDS: usize = 24; +const RATE: usize = 136; + +const ROUND_CONSTANTS: [u64; ROUNDS] = [ + 0x0000_0000_0000_0001, + 0x0000_0000_0000_8082, + 0x8000_0000_0000_808a, + 0x8000_0000_8000_8000, + 0x0000_0000_0000_808b, + 0x0000_0000_8000_0001, + 0x8000_0000_8000_8081, + 0x8000_0000_0000_8009, + 0x0000_0000_0000_008a, + 0x0000_0000_0000_0088, + 0x0000_0000_8000_8009, + 0x0000_0000_8000_000a, + 0x0000_0000_8000_808b, + 0x8000_0000_0000_008b, + 0x8000_0000_0000_8089, + 0x8000_0000_0000_8003, + 0x8000_0000_0000_8002, + 0x8000_0000_0000_0080, + 0x0000_0000_0000_800a, + 0x8000_0000_8000_000a, + 0x8000_0000_8000_8081, + 0x8000_0000_0000_8080, + 0x0000_0000_8000_0001, + 0x8000_0000_8000_8008, +]; + +const ROTATION_OFFSETS: [u32; 25] = [ + 0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14, +]; + +/// Computes Ethereum Keccak-256, not FIPS SHA3-256. +pub fn keccak256(input: &[u8]) -> [u8; 32] { + let mut state = [0u64; 25]; + let mut chunks = input.chunks_exact(RATE); + for block in chunks.by_ref() { + absorb_block(&mut state, block); + keccak_f1600(&mut state); + } + + let rem = chunks.remainder(); + let mut block = [0u8; RATE]; + block[..rem.len()].copy_from_slice(rem); + block[rem.len()] ^= 0x01; + block[RATE - 1] ^= 0x80; + absorb_block(&mut state, &block); + keccak_f1600(&mut state); + + let mut out = [0u8; 32]; + for (index, byte) in out.iter_mut().enumerate() { + *byte = ((state[index / 8] >> (8 * (index % 8))) & 0xff) as u8; + } + out +} + +fn absorb_block(state: &mut [u64; 25], block: &[u8]) { + for (lane, bytes) in block.chunks_exact(8).enumerate() { + state[lane] ^= u64::from_le_bytes(bytes.try_into().expect("lane is eight bytes")); + } +} + +fn keccak_f1600(state: &mut [u64; 25]) { + for &rc in &ROUND_CONSTANTS { + let mut c = [0u64; 5]; + for x in 0..5 { + c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]; + } + + let mut d = [0u64; 5]; + for x in 0..5 { + d[x] = c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1); + } + for y in 0..5 { + for x in 0..5 { + state[x + 5 * y] ^= d[x]; + } + } + + let mut b = [0u64; 25]; + for y in 0..5 { + for x in 0..5 { + let idx = x + 5 * y; + let dst = y + 5 * ((2 * x + 3 * y) % 5); + b[dst] = state[idx].rotate_left(ROTATION_OFFSETS[idx]); + } + } + + for y in 0..5 { + for x in 0..5 { + state[x + 5 * y] = + b[x + 5 * y] ^ ((!b[((x + 1) % 5) + 5 * y]) & b[((x + 2) % 5) + 5 * y]); + } + } + + state[0] ^= rc; + } +} + +#[cfg(test)] +mod tests { + use super::keccak256; + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() + } + + #[test] + fn keccak256_known_vectors() { + assert_eq!( + hex(&keccak256(b"")), + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + ); + assert_eq!( + hex(&keccak256(b"abc")), + "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45" + ); + assert_eq!( + &hex(&keccak256(b"transfer(address,uint256)"))[..8], + "a9059cbb" + ); + } +} diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 63603578..dc644872 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -1,10 +1,116 @@ +//! Shared high-level intermediate representation for Solcore. +//! +//! This crate owns syntax-independent compiler data that later phases can +//! query through Salsa: source inputs, HIR nodes, definition identity, name +//! resolution summaries, diagnostics, and source spans. Parser and driver +//! crates build on this crate, but the HIR layer deliberately stays unaware of +//! parsing so that semantic queries can depend on stable, lowered structures. +//! +//! Spans in HIR are anchor-relative. They carry enough identity to survive byte +//! shifts near a definition, but absolute file positions are resolved only at +//! the outer diagnostic/LSP boundary through [`Db::def_location_table`]. + +/// Definition identity and def-anchor location tables. pub mod anchor; +/// Small typed arenas used by lowered function bodies. pub mod arena; +/// Lowered syntax tree nodes. pub mod ast; +/// Diagnostic values and rendering support. pub mod diag; +/// Salsa inputs for source files and compilation roots. pub mod input; +/// Ethereum Keccak-256 helper for selector and literal hashing. +pub mod keccak; +/// Intra-module name resolution. +pub mod nameres; +/// Semantic model types. pub mod sema; +/// Anchor-relative source spans. pub mod span; +/// HIR visitors and validation helpers. +pub mod visit; + +/// Converts a file URL to a local path on native targets and wasm. +/// +/// Solcore's virtual VFS paths are platform-neutral even though they are +/// represented as `file:` URLs. Native builds prefer +/// [`url::Url::to_file_path`], then decode a local URL directly when the native +/// conversion rejects a drive-less URL such as `file:///main/main.solc` on +/// Windows. The `url` crate cfg-gates its native conversion API off for +/// `wasm32-unknown-unknown`, so wasm builds use the direct form as well. +pub fn url_to_file_path(url: &url::Url) -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + url.to_file_path() + .ok() + .or_else(|| decoded_local_file_url_path(url)) + } + + #[cfg(target_arch = "wasm32")] + { + decoded_local_file_url_path(url) + } +} +fn decoded_local_file_url_path(url: &url::Url) -> Option { + if url.scheme() != "file" || url.host_str().is_some() { + return None; + } + let decoded = percent_encoding::percent_decode_str(url.path()) + .decode_utf8() + .ok()?; + Some(std::path::PathBuf::from(decoded.as_ref())) +} + +/// Database contract required by HIR queries and boundary utilities. +/// +/// The trait is intentionally small. HIR owns the span and identity types, but +/// the parser produces the per-file def-location table, so concrete databases +/// inject that table here without creating a crate cycle. #[salsa::db] -pub trait Db: salsa::Database {} +pub trait Db: salsa::Database { + /// Returns the base-offset table for the def anchors of `file`. + /// + /// Lowering produces this table (`parser::parse_file_to_hir`), which lives + /// *above* `hir` in the crate graph, so the concrete database wires this by + /// delegating to the parser (dependency injection, rust-analyzer + /// `Upcast`-style). Callers must only invoke this at the diagnostic/LSP + /// edge — never inside a tracked query — otherwise anchor-relative spans + /// would leak absolute offsets into the Salsa cache and over-invalidate. + fn def_location_table<'db>( + &'db self, + file: crate::input::SourceFile, + ) -> &'db crate::anchor::DefLocationTable<'db>; +} + +#[cfg(test)] +mod url_to_file_path_tests { + use std::path::Path; + + use super::*; + + #[test] + fn virtual_file_urls_are_platform_neutral() { + for (url, expected) in [ + ("file:///main/main.solc", "/main/main.solc"), + ("file:///std/std.solc", "/std/std.solc"), + ("file:///ext/math/lib.solc", "/ext/math/lib.solc"), + ("file:///main/space%20name.solc", "/main/space name.solc"), + ] { + let url = url::Url::parse(url).expect("virtual file URL"); + assert_eq!( + decoded_local_file_url_path(&url).as_deref(), + Some(Path::new(expected)) + ); + assert_eq!(url_to_file_path(&url).as_deref(), Some(Path::new(expected))); + } + } + + #[test] + fn direct_file_url_decoding_rejects_a_remote_host() { + let remote = url::Url::parse("file://server/main/file.solc").expect("remote URL"); + + assert!(decoded_local_file_url_path(&remote).is_none()); + } +} diff --git a/crates/hir/src/nameres/body_resolver.rs b/crates/hir/src/nameres/body_resolver.rs new file mode 100644 index 00000000..27383757 --- /dev/null +++ b/crates/hir/src/nameres/body_resolver.rs @@ -0,0 +1,917 @@ +use super::*; + +pub(super) struct BodyResolver<'db, 'a> { + db: &'db dyn Db, + scope: &'a ItemScopeFacts<'db>, + imports: &'a dyn ImportedNames<'db>, + contract: Option>, + local_scopes: Vec>>, + type_vars: Vec>, + pub(super) map: BodyResolutionMap<'db>, +} + +impl<'db, 'a> BodyResolver<'db, 'a> { + pub(super) fn new( + db: &'db dyn Db, + scope: &'a ItemScopeFacts<'db>, + imports: &'a dyn ImportedNames<'db>, + contract: Option>, + ) -> Self { + Self { + db, + scope, + imports, + contract, + local_scopes: Vec::new(), + type_vars: Vec::new(), + map: BodyResolutionMap::default(), + } + } + + pub(super) fn body(&mut self, body: FuncBody<'db>) { + for stmt in body.top_level_stmts(self.db) { + self.stmt(body, *stmt); + } + } + + fn stmt(&mut self, body: FuncBody<'db>, stmt_id: Id>) { + let stmt = body.stmts(self.db).get(stmt_id); + match &stmt.kind { + StmtKind::Let { name, ty, init, .. } => { + if let Some(ty) = ty { + self.ty(*ty); + } + if let Some(init) = init { + // Reference semantics: a let initializer is evaluated in + // the pre-binder scope, so the new local is inserted after + // the initializer has been resolved. + self.expr(body, *init); + } + let resolution = Resolution::Local(LocalBinding::Let { + body, + stmt: stmt_id, + }); + self.add_local(ident_text_str(self.db, name), resolution.clone()); + self.map.record_stmt(body, stmt_id, resolution); + } + StmtKind::Return(expr) => { + if let Some(expr) = expr { + self.expr(body, *expr); + } + } + StmtKind::Expr(expr) => self.expr(body, *expr), + StmtKind::Assign { lhs, rhs, .. } => { + self.expr(body, *lhs); + self.expr(body, *rhs); + } + StmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + self.expr(body, *scrutinee); + } + for arm in arms { + self.match_arm(body, arm); + } + } + StmtKind::For { + init, + cond, + post, + body: for_body, + } => { + // `for` does not create a lexical scope; initializer, condition, + // post statements, and body share the surrounding scope. + for stmt in init { + self.stmt(body, *stmt); + } + self.expr(body, *cond); + for stmt in post { + self.stmt(body, *stmt); + } + for stmt in for_body { + self.stmt(body, *stmt); + } + } + StmtKind::If { + cond, + then_body, + else_body, + } => { + self.expr(body, *cond); + for stmt in then_body { + self.stmt(body, *stmt); + } + if let Some(else_body) = else_body { + for stmt in else_body { + self.stmt(body, *stmt); + } + } + } + StmtKind::Block { body: block } => { + self.with_scope(|resolver| { + for stmt in block { + resolver.stmt(body, *stmt); + } + }); + } + StmtKind::Assembly { .. } | StmtKind::Break | StmtKind::Continue | StmtKind::Error => {} + } + } + + fn match_arm(&mut self, body: FuncBody<'db>, arm: &MatchArm<'db>) { + self.with_scope(|resolver| { + for pat in &arm.pats { + resolver.pat(body, *pat); + } + for stmt in &arm.body { + resolver.stmt(body, *stmt); + } + }); + } + + fn expr(&mut self, body: FuncBody<'db>, expr_id: Id>) { + let expr = body.exprs(self.db).get(expr_id); + match &expr.kind { + ExprKind::Lit(_) => {} + ExprKind::Error => { + self.map.record_expr(body, expr_id, Resolution::Err); + } + ExprKind::Ident(name) => { + let resolution = self.resolve_ident(name); + self.map.record_expr(body, expr_id, resolution); + } + ExprKind::DotCtor { name, args, .. } => { + for arg in args { + self.expr(body, *arg); + } + let leaf = ident_text_str(self.db, name); + let resolution = if self.has_constructor_leaf(leaf) { + Resolution::DotCtorDeferred + } else if self.imports.may_contain_unknown_unqualified( + self.db, + Namespace::Term, + leaf, + ) { + Resolution::Err + } else { + self.map.diagnostics.push(self.undefined_name_diag( + leaf, + name.span(self.db), + UndefinedNameKind::Other, + )); + Resolution::Err + }; + self.map.record_expr(body, expr_id, resolution); + } + ExprKind::Proxy { ty, .. } => self.ty(*ty), + ExprKind::Lambda { + params, + ret, + body: lambda_body, + } => { + for param in params.atom() { + self.param_type(param); + } + if let Some(ret) = ret { + self.ty(*ret); + } + self.with_scope(|resolver| { + for (index, param) in params.atom().iter().enumerate() { + if let Some(name) = param_name(param) { + resolver.add_param(*lambda_body, index, name); + } + } + resolver.body(*lambda_body); + }); + } + ExprKind::BinOp { lhs, rhs, .. } => { + self.expr(body, *lhs); + self.expr(body, *rhs); + } + ExprKind::Index { base, index } => { + self.expr(body, *base); + self.expr(body, *index); + } + ExprKind::Call { callee, args } => { + self.call_callee(body, *callee); + for arg in args { + self.expr(body, *arg); + } + } + ExprKind::Field { base, field } => { + if self.is_namespace_qualifier(body, *base) { + let access_path = + expr_path(self.db, body, expr_id).map(|segments| segments.join(".")); + self.expr_as_qualifier(body, *base, access_path.as_deref()); + } else { + self.expr(body, *base); + } + if let Some(resolution) = self.resolve_field_expr(body, *base, field) { + self.map.record_expr(body, expr_id, resolution); + } + } + ExprKind::Conversion { expr, ty } | ExprKind::TypeAscription { expr, ty } => { + self.expr(body, *expr); + self.ty(*ty); + } + ExprKind::UnaryOp { expr, .. } => self.expr(body, *expr), + ExprKind::If { + cond, + then_expr, + else_expr, + } => { + self.expr(body, *cond); + self.expr(body, *then_expr); + self.expr(body, *else_expr); + } + ExprKind::Tuple(elems) => { + for elem in elems { + self.expr(body, *elem); + } + } + } + } + + fn pat(&mut self, body: FuncBody<'db>, pat_id: Id>) { + let pat = body.pats(self.db).get(pat_id); + match &pat.kind { + PatKind::Wildcard | PatKind::Lit(_) => {} + PatKind::Error => { + self.map.record_pat(body, pat_id, Resolution::Err); + } + PatKind::Var(name) => { + let leaf = ident_text_str(self.db, name); + let resolution = if let Some( + res @ Resolution::Builtin(BuiltinKind::Constructor( + BuiltinCtor::True | BuiltinCtor::False, + )), + ) = builtin_term(leaf) + { + res + } else if self.has_user_constructor_leaf(leaf) { + // Every in-scope user constructor must be written + // qualified; silently binding it as a variable would turn + // the arm into a catch-all. + self.map.diagnostics.push(unqualified_constructor( + self.db, + leaf, + name.span(self.db), + self.constructor_qualification(leaf), + )); + Resolution::Err + } else { + let resolution = Resolution::Local(LocalBinding::Pattern { body, pat: pat_id }); + self.add_local(leaf, resolution.clone()); + resolution + }; + self.map.record_pat(body, pat_id, resolution); + } + PatKind::Ctor { head, args } => { + for arg in args { + self.pat(body, *arg); + } + let resolution = match head { + PatCtorHead::Deferred { .. } => Resolution::DotCtorDeferred, + PatCtorHead::Qualified { qualifier, name } => { + let qualifier_text = ident_text_str(self.db, qualifier); + let qualified = qualify(qualifier_text, ident_text_str(self.db, name)); + self.lookup_ctor(&qualified).unwrap_or_else(|| { + if self + .imports + .has_incomplete_module_qualifier(self.db, qualifier_text) + { + return Resolution::Err; + } + let kind = if self.lookup_type(qualifier_text).is_some() + || self.lookup_module(qualifier_text).is_some() + { + UndefinedNameKind::Field + } else { + UndefinedNameKind::QualifiedConstructor { + access_path: qualified.clone(), + } + }; + self.map.diagnostics.push(self.undefined_name_diag( + &qualified, + name.span(self.db), + kind, + )); + Resolution::Err + }) + } + PatCtorHead::Unqualified { name } => { + let leaf = ident_text_str(self.db, name); + if self.has_user_constructor_leaf(leaf) { + self.map.diagnostics.push(unqualified_constructor( + self.db, + leaf, + name.span(self.db), + self.constructor_qualification(leaf), + )); + Resolution::Err + } else if matches!( + builtin_term(leaf), + Some(Resolution::Builtin(BuiltinKind::Constructor(_))) + ) { + // Primitive constructors (`pair`, `inl`, ...) stay + // legal unqualified; their concrete constructor is + // picked from the expected type during inference. + Resolution::DotCtorDeferred + } else if self.imports.may_contain_unknown_unqualified( + self.db, + Namespace::Term, + leaf, + ) { + Resolution::Err + } else if args.is_empty() { + let resolution = + Resolution::Local(LocalBinding::Pattern { body, pat: pat_id }); + self.add_local(leaf, resolution.clone()); + resolution + } else { + self.map + .diagnostics + .push(invalid_pattern(self.db, pat.span)); + Resolution::Err + } + } + }; + self.map.record_pat(body, pat_id, resolution); + } + PatKind::ComptimeLabel { expr, .. } => self.expr(body, *expr), + PatKind::Tuple { elems } => { + for elem in elems { + self.pat(body, *elem); + } + } + } + } + + fn ty(&mut self, ty: TypeRef<'db>) { + match ty.kind(self.db) { + TypeRefKind::Named { + qualifier, + name, + args, + } => { + for arg in args.atom() { + self.ty(*arg); + } + let resolution = if let Some(qualifier) = qualifier { + let qualifier_text = ident_text_str(self.db, qualifier); + let qualified = qualify(qualifier_text, ident_text_str(self.db, name)); + self.lookup_type(&qualified).unwrap_or_else(|| { + if self + .imports + .has_incomplete_module_qualifier(self.db, qualifier_text) + { + return Resolution::Err; + } + self.map + .diagnostics + .push(self.undefined_type_ctor_diag(&qualified, name.span(self.db))); + Resolution::Err + }) + } else { + let name_text = ident_text_str(self.db, name); + self.lookup_type(name_text).unwrap_or_else(|| { + self.map + .diagnostics + .push(self.undefined_type_ctor_diag(name_text, name.span(self.db))); + Resolution::Err + }) + }; + self.map.types.push(TypeResolution { ty, resolution }); + } + TypeRefKind::FixedArray { element, .. } => self.ty(*element), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + self.ty(*param); + } + self.ty(*ret); + } + TypeRefKind::Comptime { inner, .. } => self.ty(*inner), + TypeRefKind::Tuple { elems } => { + for elem in elems.atom() { + self.ty(*elem); + } + } + TypeRefKind::Error { .. } => { + self.map.types.push(TypeResolution { + ty, + resolution: Resolution::Err, + }); + } + } + } + + fn param_type(&mut self, param: &FuncParam<'db>) { + if let FuncParam::Typed { ty, .. } = param { + self.ty(*ty); + } + } + + fn resolve_ident(&mut self, name: &SpannedElem<'db, Ident<'db>>) -> Resolution<'db> { + let text = ident_text_str(self.db, name); + if let Some(resolution) = self + .lookup_local(text) + // Contract fields intentionally beat same-name functions in the + // contract term surface. + .or_else(|| self.lookup_field(text)) + .or_else(|| self.lookup_qualified_term(text)) + .or_else(|| self.lookup_unqualified_class_method(text)) + { + if matches!(&resolution, Resolution::Ctor { .. }) { + return self.reject_unqualified_constructor(name); + } + return resolution; + } + if self.has_user_constructor_leaf(text) { + return self.reject_unqualified_constructor(name); + } + self.lookup_type(text) + .or_else(|| self.lookup_module(text)) + .unwrap_or_else(|| { + if self + .imports + .may_contain_unknown_unqualified(self.db, Namespace::Term, text) + { + return Resolution::Err; + } + self.map.diagnostics.push(self.undefined_name_diag( + text, + name.span(self.db), + UndefinedNameKind::Term, + )); + Resolution::Err + }) + } + + fn call_callee(&mut self, body: FuncBody<'db>, expr_id: Id>) { + let expr = body.exprs(self.db).get(expr_id); + match &expr.kind { + ExprKind::Ident(name) => { + let resolution = self.resolve_call_ident(name); + self.map.record_expr(body, expr_id, resolution); + } + _ => self.expr(body, expr_id), + } + } + + fn resolve_call_ident(&mut self, name: &SpannedElem<'db, Ident<'db>>) -> Resolution<'db> { + let text = ident_text_str(self.db, name); + if let Some(resolution) = self + .lookup_local(text) + .or_else(|| self.lookup_qualified_term(text)) + .or_else(|| self.lookup_field(text)) + .or_else(|| self.lookup_unqualified_class_method(text)) + { + if matches!(&resolution, Resolution::Ctor { .. }) { + return self.reject_unqualified_constructor(name); + } + return resolution; + } + self.resolve_ident(name) + } + + fn reject_unqualified_constructor( + &mut self, + name: &SpannedElem<'db, Ident<'db>>, + ) -> Resolution<'db> { + let text = ident_text_str(self.db, name); + self.map.diagnostics.push(unqualified_constructor( + self.db, + text, + name.span(self.db), + self.constructor_qualification(text), + )); + Resolution::Err + } + + fn expr_as_qualifier( + &mut self, + body: FuncBody<'db>, + expr_id: Id>, + access_path: Option<&str>, + ) { + let expr = body.exprs(self.db).get(expr_id); + match &expr.kind { + ExprKind::Ident(name) => { + let text = ident_text_str(self.db, name); + let resolution = self + .lookup_type(text) + .or_else(|| self.lookup_module(text)) + .or_else(|| self.lookup_qualified_term(text)) + .unwrap_or_else(|| { + if self.imports.may_contain_unknown_unqualified( + self.db, + Namespace::Module, + text, + ) { + return Resolution::Err; + } + self.map.diagnostics.push(self.undefined_name_diag( + text, + name.span(self.db), + UndefinedNameKind::ModuleQualifier { + access_path: access_path.unwrap_or(text).to_owned(), + }, + )); + Resolution::Err + }); + self.map.record_expr(body, expr_id, resolution); + } + ExprKind::Field { base, field } => { + self.expr_as_qualifier(body, *base, access_path); + if let Some(resolution) = self.resolve_field_expr(body, *base, field) { + self.map.record_expr(body, expr_id, resolution); + } + } + _ => self.expr(body, expr_id), + } + } + + fn resolve_field_expr( + &mut self, + body: FuncBody<'db>, + base: Id>, + field: &SpannedElem<'db, Ident<'db>>, + ) -> Option> { + let path = expr_path(self.db, body, base)?; + let first = path.first()?; + if self.lookup_local(first).is_some() || self.lookup_field(first).is_some() { + return None; + } + let qualifier = path.join("."); + let field_text = ident_text_str(self.db, field); + let qualified = qualify(&qualifier, field_text); + + if let Some(resolution) = self.lookup_qualified_term(&qualified) { + return Some(resolution); + } + + if let Some(resolution) = self.lookup_type(&qualified) { + return Some(resolution); + } + + if matches!( + self.lookup_type(&qualifier), + Some( + Resolution::Def { + kind: DefResolutionKind::Adt + | DefResolutionKind::Contract + | DefResolutionKind::Class + | DefResolutionKind::TypeAlias + | DefResolutionKind::ValueType, + .. + } | Resolution::Builtin(BuiltinKind::Type(_) | BuiltinKind::Class(_)) + ) + ) { + self.map.diagnostics.push(self.undefined_name_diag( + field_text, + field.span(self.db), + UndefinedNameKind::Field, + )); + return Some(Resolution::Err); + } + + if self.lookup_module(&qualifier).is_some() { + if self.lookup_module(&qualified).is_none() { + if self + .imports + .has_incomplete_module_qualifier(self.db, &qualifier) + { + return Some(Resolution::Err); + } + let private_candidate = self.imports.private_candidate( + self.db, + Namespace::Term, + &qualifier, + field_text, + ); + self.map + .diagnostics + .push(self.undefined_name_diag_with_private( + field_text, + field.span(self.db), + UndefinedNameKind::ModuleMember { + access_path: qualified, + }, + private_candidate, + )); + return Some(Resolution::Err); + } + return Some(Resolution::Module(ModuleRef { + owner: self.scope.module.def_id_value(self.db), + name: qualified, + })); + } + + None + } + + fn undefined_name_diag( + &self, + name: &str, + span: Span<'db>, + kind: UndefinedNameKind, + ) -> NameresDiagnostic { + self.undefined_name_diag_with_private(name, span, kind, None) + } + + fn undefined_name_diag_with_private( + &self, + name: &str, + span: Span<'db>, + kind: UndefinedNameKind, + private_candidate: Option, + ) -> NameresDiagnostic { + let suggestion = private_candidate + .is_none() + .then(|| best_name_suggestion(name, self.name_candidate_names())) + .flatten(); + undefined_name(self.db, name, span, kind, suggestion, private_candidate) + } + + fn undefined_type_ctor_diag(&self, name: &str, span: Span<'db>) -> NameresDiagnostic { + let constructor_candidate = unique_constructor_type_candidate( + self.constructor_type_candidates(name) + .into_iter() + .filter(|candidate| candidate.ctor_name == name), + ); + let suggestion = constructor_candidate + .is_none() + .then(|| best_name_suggestion(name, self.type_candidate_names())) + .flatten(); + undefined_type_ctor(self.db, name, span, suggestion, constructor_candidate) + } + + fn constructor_qualification(&self, leaf: &str) -> Option { + unique_constructor_type_candidate( + self.constructor_type_candidates(leaf) + .into_iter() + .filter(|candidate| candidate.ctor_name == leaf), + ) + .map(|candidate| qualify(&candidate.ty_name, &candidate.ctor_name)) + } + + fn name_candidate_names(&self) -> Vec { + let mut names = Vec::new(); + for scope in &self.local_scopes { + names.extend(scope.keys().cloned()); + } + if let Some(contract) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + { + names.extend(contract.fields.iter().map(|entry| entry.name.clone())); + names.extend( + contract + .terms + .iter() + .filter(|entry| contract.is_unqualified_term_visible(entry)) + .map(|entry| entry.name.clone()), + ); + names.extend(contract.types.iter().map(|entry| entry.name.clone())); + } + names.extend( + self.scope + .terms + .iter() + .filter(|entry| !self.is_self_external_resolution(&entry.resolution)) + .map(|entry| entry.name.clone()), + ); + names.extend(self.scope.types.iter().map(|entry| entry.name.clone())); + names.extend(self.scope.modules.iter().map(|entry| entry.name.clone())); + names.extend(self.imports.candidate_names(self.db, Namespace::Term)); + names.extend(self.imports.candidate_names(self.db, Namespace::Type)); + names.extend(self.imports.candidate_names(self.db, Namespace::Module)); + names + } + + fn type_candidate_names(&self) -> Vec { + let mut names = Vec::new(); + names.extend( + self.type_vars + .iter() + .map(|var| ident_text_str(self.db, &var.name).to_owned()), + ); + if let Some(contract) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + { + names.extend(contract.types.iter().map(|entry| entry.name.clone())); + } + names.extend(self.scope.types.iter().map(|entry| entry.name.clone())); + names.extend(self.imports.candidate_names(self.db, Namespace::Type)); + names + } + + fn constructor_type_candidates(&self, leaf: &str) -> Vec { + let mut candidates = Vec::new(); + if let Some(contract) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + { + collect_constructor_type_candidates( + self.db, + &contract.ctor_lists, + leaf, + &mut candidates, + ); + } + collect_constructor_type_candidates(self.db, &self.scope.ctor_lists, leaf, &mut candidates); + candidates.extend(self.imports.constructor_type_candidates(self.db, leaf)); + candidates + } + + fn lookup_qualified_term(&self, name: &str) -> Option> { + if let Some(resolution) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + .and_then(|contract| contract.term_resolution(name)) + { + return Some(resolution); + } + let resolution = self + .scope + .term_resolution(name) + .or_else(|| self.imports.imported(self.db, Namespace::Term, name)) + .or_else(|| builtin_term(name))?; + if self.is_self_external_resolution(&resolution) { + return None; + } + Some(resolution) + } + + fn is_self_external_resolution(&self, resolution: &Resolution<'db>) -> bool { + self.contract + .and_then(|contract| self.scope.contract_scope(contract)) + .is_some_and(|contract| contract.is_external_function_resolution(resolution)) + } + + fn lookup_unqualified_class_method(&self, name: &str) -> Option> { + let mut matches = self + .scope + .terms + .iter() + .filter(|entry| entry.name.rsplit('.').next() == Some(name)) + .filter_map(|entry| match &entry.resolution { + Resolution::ClassMethod { .. } => Some(entry.resolution.clone()), + _ => None, + }); + let first = matches.next()?; + if matches.next().is_some() { + return None; + } + Some(first) + } + + fn lookup_ctor(&self, name: &str) -> Option> { + match self.lookup_qualified_term(name) { + Some(res @ Resolution::Ctor { .. }) + | Some(res @ Resolution::Builtin(BuiltinKind::Constructor(_))) => Some(res), + _ => None, + } + } + + fn lookup_local(&self, name: &str) -> Option> { + self.local_scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + } + + fn lookup_field(&self, name: &str) -> Option> { + self.contract + .and_then(|contract| self.scope.contract_scope(contract)) + .and_then(|contract| contract.field_resolution(name)) + } + + fn lookup_type(&self, name: &str) -> Option> { + self.type_vars + .iter() + .rev() + .find(|var| ident_text_str(self.db, &var.name) == name) + .map(|var| { + Resolution::Local(LocalBinding::TypeVar(TypeVarId { + owner: var.owner, + index: var.index, + name: name.to_owned(), + })) + }) + .or_else(|| { + self.contract + .and_then(|contract| self.scope.contract_scope(contract)) + .and_then(|contract| contract.type_resolution(name)) + }) + .or_else(|| self.scope.type_resolution(name)) + .or_else(|| self.imports.imported(self.db, Namespace::Type, name)) + .or_else(|| builtin_type_or_class(name)) + .or_else(|| { + self.imports + .may_contain_unknown_unqualified(self.db, Namespace::Type, name) + .then_some(Resolution::Err) + }) + } + + fn lookup_module(&self, name: &str) -> Option> { + self.scope + .module_resolution(name) + .or_else(|| self.imports.imported(self.db, Namespace::Module, name)) + } + + fn has_constructor_leaf(&self, leaf: &str) -> bool { + self.has_user_constructor_leaf(leaf) + || matches!( + builtin_term(leaf), + Some(Resolution::Builtin(BuiltinKind::Constructor(_))) + ) + } + + /// Returns whether any user-declared constructor in scope has this leaf + /// name, excluding the builtin (primitive) constructors. + /// + /// Unqualified references to such constructors are rejected with `SC0106`, + /// while primitive constructors stay legal unqualified. + fn has_user_constructor_leaf(&self, leaf: &str) -> bool { + self.contract + .and_then(|contract| self.scope.contract_scope(contract)) + .is_some_and(|contract| contract.has_constructor_leaf(leaf)) + || self.scope.has_constructor_leaf(leaf) + || self.imports.has_constructor_leaf(self.db, leaf) + } + + fn is_namespace_qualifier(&self, body: FuncBody<'db>, expr: Id>) -> bool { + let Some(path) = expr_path(self.db, body, expr) else { + return false; + }; + let Some(first) = path.first() else { + return false; + }; + if path.len() == 1 + && (self.lookup_local(first).is_some() || self.lookup_field(first).is_some()) + { + return false; + } + if self.lookup_type(first).is_some() || self.lookup_module(first).is_some() { + return true; + } + + if self.lookup_local(first).is_some() + || self.lookup_field(first).is_some() + || self.lookup_qualified_term(first).is_some() + || self.lookup_unqualified_class_method(first).is_some() + { + return false; + } + + // A path-shaped expression whose base is still unresolved is not a + // bare term lookup. Resolve it as a qualifier so typed diagnostics do + // not expose its first segment as an auto-importable term. If the base + // later becomes a value, the checks above keep ordinary member access + // on the value-expression path. + true + } + + fn add_local(&mut self, name: &str, resolution: Resolution<'db>) { + if let Some(scope) = self.local_scopes.last_mut() { + scope.insert(name.to_owned(), resolution); + } else { + let mut scope = FxHashMap::default(); + scope.insert(name.to_owned(), resolution); + self.local_scopes.push(scope); + } + } + + pub(super) fn add_param( + &mut self, + body: FuncBody<'db>, + index: usize, + name: &SpannedElem<'db, Ident<'db>>, + ) { + self.add_local( + ident_text_str(self.db, name), + Resolution::Param(ParamId { + body, + index: ParamIndex::from_usize(index), + }), + ); + } + + pub(super) fn with_scope(&mut self, f: impl FnOnce(&mut Self)) { + self.local_scopes.push(FxHashMap::default()); + f(self); + self.local_scopes.pop(); + } + + pub(super) fn with_type_vars( + &mut self, + vars: &[TypeVarBinding<'db>], + f: impl FnOnce(&mut Self), + ) { + let old_len = self.type_vars.len(); + self.type_vars.extend_from_slice(vars); + f(self); + self.type_vars.truncate(old_len); + } +} diff --git a/crates/hir/src/nameres/builtins.rs b/crates/hir/src/nameres/builtins.rs new file mode 100644 index 00000000..3a2e3959 --- /dev/null +++ b/crates/hir/src/nameres/builtins.rs @@ -0,0 +1,93 @@ +use super::*; + +pub(super) fn best_name_suggestion( + name: &str, + candidates: impl IntoIterator, +) -> Option { + let mut candidates = candidates + .into_iter() + .filter(|candidate| candidate != name) + .collect::>(); + candidates.sort(); + candidates.dedup(); + + let mut best: Option<(usize, String)> = None; + for candidate in candidates { + let distance = edit_distance(name, &candidate); + let limit = suggestion_distance_limit(name, &candidate); + if distance == 0 || distance > limit { + continue; + } + match &best { + Some((best_distance, best_candidate)) + if distance > *best_distance + || (distance == *best_distance && candidate >= *best_candidate) => {} + _ => best = Some((distance, candidate)), + } + } + best.map(|(_, candidate)| candidate) +} + +fn suggestion_distance_limit(left: &str, right: &str) -> usize { + let max_len = left.chars().count().max(right.chars().count()); + if max_len <= 4 { 1 } else { 3 } +} + +fn edit_distance(left: &str, right: &str) -> usize { + let right_chars = right.chars().collect::>(); + let mut previous = (0..=right_chars.len()).collect::>(); + let mut current = vec![0; right_chars.len() + 1]; + + for (left_index, left_char) in left.chars().enumerate() { + current[0] = left_index + 1; + for (right_index, right_char) in right_chars.iter().enumerate() { + let substitution = usize::from(left_char != *right_char); + current[right_index + 1] = (previous[right_index + 1] + 1) + .min(current[right_index] + 1) + .min(previous[right_index] + substitution); + } + previous.clone_from(¤t); + } + + previous[right_chars.len()] +} + +pub(super) fn builtin_type_or_class<'db>(name: &str) -> Option> { + let kind = match name { + "word" | "Word" => BuiltinKind::Type(BuiltinType::Word), + "bool" => BuiltinKind::Type(BuiltinType::Bool), + "()" => BuiltinKind::Type(BuiltinType::Unit), + "pair" => BuiltinKind::Type(BuiltinType::Pair), + "sum" => BuiltinKind::Type(BuiltinType::Sum), + "integer" => BuiltinKind::Type(BuiltinType::Integer), + "invokable" => BuiltinKind::Class(BuiltinClass::Invokable), + "Int" => BuiltinKind::Class(BuiltinClass::Int), + _ => return None, + }; + Some(Resolution::Builtin(kind)) +} + +pub(super) fn builtin_term<'db>(name: &str) -> Option> { + let kind = match name { + "true" => BuiltinKind::Constructor(BuiltinCtor::True), + "false" => BuiltinKind::Constructor(BuiltinCtor::False), + "()" => BuiltinKind::Constructor(BuiltinCtor::Unit), + "pair" => BuiltinKind::Constructor(BuiltinCtor::Pair), + "inl" => BuiltinKind::Constructor(BuiltinCtor::Inl), + "inr" => BuiltinKind::Constructor(BuiltinCtor::Inr), + "invoke" => BuiltinKind::Function(BuiltinFunction::Invoke), + "primAddWord" => BuiltinKind::Function(BuiltinFunction::PrimAddWord), + "primEqWord" => BuiltinKind::Function(BuiltinFunction::PrimEqWord), + "wordToInteger" => BuiltinKind::Function(BuiltinFunction::WordToInteger), + "wordFromInteger" => BuiltinKind::Function(BuiltinFunction::WordFromInteger), + "integerAdd" => BuiltinKind::Function(BuiltinFunction::IntegerAdd), + "integerSub" => BuiltinKind::Function(BuiltinFunction::IntegerSub), + "integerMul" => BuiltinKind::Function(BuiltinFunction::IntegerMul), + "integerLt" => BuiltinKind::Function(BuiltinFunction::IntegerLt), + "integerEq" => BuiltinKind::Function(BuiltinFunction::IntegerEq), + "invokable.invoke" => BuiltinKind::ClassMethod(BuiltinClassMethod::InvokableInvoke), + "Int.fromInteger" => BuiltinKind::ClassMethod(BuiltinClassMethod::IntFromInteger), + _ => return None, + }; + Some(Resolution::Builtin(kind)) +} diff --git a/crates/hir/src/nameres/diagnostic.rs b/crates/hir/src/nameres/diagnostic.rs new file mode 100644 index 00000000..c56b40e0 --- /dev/null +++ b/crates/hir/src/nameres/diagnostic.rs @@ -0,0 +1,347 @@ +use super::*; + +/// Lookup context for an `SC0101` undefined-name diagnostic. +/// +/// This stays on the typed name-resolution diagnostic instead of the generic +/// rendering surface so semantic clients can distinguish auto-importable bare +/// terms from names whose spelling occurs in a different lookup position. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum UndefinedNameKind { + /// An unqualified term lookup, such as a function or value expression. + Term, + /// An unresolved module or type qualifier in a qualified access. + /// + /// The access path keeps every source segment, including the selected + /// member, so semantic clients can distinguish `Option.Some` from + /// `math.value` without reconstructing syntax from a diagnostic span. + ModuleQualifier { + /// Dotted access path exactly as written in the source. + access_path: String, + }, + /// An unresolved qualified constructor pattern. + QualifiedConstructor { + /// Dotted constructor path exactly as written in the source. + access_path: String, + }, + /// A missing member after a resolved module qualifier. + ModuleMember { + /// Dotted module-member access path exactly as written in the source. + access_path: String, + }, + /// A member lookup after a resolved type or value path. + Field, + /// A specialized lookup that is not safely treated as a bare term. + Other, +} + +/// Typed local name-resolution diagnostic. +/// +/// The variants mirror the `SC010x` local resolver codes and store +/// lifetime-free label spans. Lowering to the generic user-facing diagnostic is +/// deferred until the driver or another diagnostic edge asks for it. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum NameresDiagnostic { + /// `SC0101`: failed term, field, module, or qualified-name lookup. + UndefinedName { + /// Name text as it appeared at the failing lookup. + name: String, + /// Source span of the failed lookup. + span: LabelSpan, + /// Semantic position in which lookup failed. + kind: UndefinedNameKind, + /// Nearest visible name, when one is close enough to be actionable. + suggestion: Option, + /// Exact private imported item hidden behind a module qualifier. + private_candidate: Option, + }, + /// `SC0103`: failed type-constructor lookup. + UndefinedTypeConstructor { + /// Type constructor name. + name: String, + /// Source span of the failed lookup. + span: LabelSpan, + /// Nearest visible type name, when one is close enough to be + /// actionable. + suggestion: Option, + /// Constructor with this name, when a value constructor was used as a + /// type. + constructor_candidate: Option, + }, + /// `SC0105`: failed class lookup. + UndefinedClass { + /// Class name. + name: String, + /// Source span of the failed lookup. + span: LabelSpan, + }, + /// `SC0106`: constructor used without the required type qualifier. + UnqualifiedConstructor { + /// Constructor leaf name. + name: String, + /// Source span of the constructor occurrence. + span: LabelSpan, + /// Concrete qualified form, when the constructor leaf has one visible + /// owner. + qualification: Option, + }, + /// `SC0107`: parser recovery produced an invalid pattern shape. + InvalidPattern { + /// Source span covering the invalid pattern. + span: LabelSpan, + }, + /// `SC0108`: duplicate declaration in a local namespace. + DuplicateDeclaration { + /// Namespace where the duplicate was found. + namespace: Namespace, + /// Duplicated surface name. + name: String, + /// Span of the duplicate declaration. + span: LabelSpan, + /// Span of the first declaration. + previous: LabelSpan, + /// Optional contextual note, such as the enclosing contract. + context: Option, + }, +} + +impl NameresDiagnostic { + /// Lowers this typed diagnostic to the generic rendering surface. + pub fn lower(&self, _db: &dyn Db) -> Diagnostic { + match self { + NameresDiagnostic::UndefinedName { + name, + span, + kind: _, + suggestion, + private_candidate, + } => { + let mut diagnostic = Diagnostic::error(format!("undefined name: {name}")) + .with_code(DiagnosticCode::NAMERES_UNDEFINED_NAME) + .with_primary_label_span(span.clone(), Some("unknown name")); + if let Some(private) = private_candidate { + diagnostic = diagnostic + .with_secondary_label_span( + private.span.clone(), + Some("private item declared here"), + ) + .with_note(format!( + "`{}` is private to module `{}` and is not exported", + private.name, private.module + )); + } + if let Some(suggestion) = suggestion { + diagnostic = diagnostic.with_help(format!("did you mean `{suggestion}`?")); + if let Some(replacement) = replacement_for_name(name, suggestion) { + diagnostic = diagnostic.with_suggestion(replace_with_suggestion( + span, + replacement, + Applicability::MaybeIncorrect, + )); + } + } + diagnostic + } + NameresDiagnostic::UndefinedTypeConstructor { + name, + span, + suggestion, + constructor_candidate, + } => { + let mut diagnostic = + Diagnostic::error(format!("undefined type constructor: {name}")) + .with_code(DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR) + .with_primary_label_span(span.clone(), Some("undefined type constructor")); + if let Some(constructor) = constructor_candidate { + diagnostic = diagnostic + .with_secondary_label_span( + constructor.span.clone(), + Some("constructor declared here"), + ) + .with_note(format!( + "`{}` is a constructor of type `{}`", + constructor.ctor_name, constructor.ty_name + )) + .with_help(format!("use `{}` as the type name", constructor.ty_name)) + .with_suggestion(replace_with_suggestion( + span, + &constructor.ty_name, + Applicability::MachineApplicable, + )); + } else if let Some(suggestion) = suggestion { + diagnostic = diagnostic.with_help(format!("did you mean type `{suggestion}`?")); + if let Some(replacement) = replacement_for_name(name, suggestion) { + diagnostic = diagnostic.with_suggestion(replace_with_suggestion( + span, + replacement, + Applicability::MaybeIncorrect, + )); + } + } + diagnostic + } + NameresDiagnostic::UndefinedClass { name, span } => { + Diagnostic::error(format!("undefined trait: {name}")) + .with_code(DiagnosticCode::NAMERES_UNDEFINED_CLASS) + .with_primary_label_span(span.clone(), Some("undefined trait")) + } + NameresDiagnostic::UnqualifiedConstructor { + name, + span, + qualification, + } => { + let help = qualification + .as_ref() + .map(|qualified| format!("use `{qualified}`")) + .unwrap_or_else(|| "use Type.Constructor form".to_owned()); + let diagnostic = Diagnostic::error(format!("unqualified constructor: {name}")) + .with_code(DiagnosticCode::NAMERES_UNQUALIFIED_CONSTRUCTOR) + .with_primary_label_span(span.clone(), Some("constructor must be qualified")) + .with_help(help); + if let Some(qualification) = qualification { + diagnostic.with_suggestion(replace_with_suggestion( + span, + qualification, + Applicability::MachineApplicable, + )) + } else { + diagnostic + } + } + NameresDiagnostic::InvalidPattern { span } => { + Diagnostic::error("invalid pattern syntax") + .with_code(DiagnosticCode::NAMERES_INVALID_PATTERN) + .with_primary_label_span(span.clone(), Some("invalid pattern")) + } + NameresDiagnostic::DuplicateDeclaration { + namespace, + name, + span, + previous, + context, + } => { + let namespace_text = match namespace { + Namespace::Type => "type namespace", + Namespace::Term => "term namespace", + Namespace::Field => "field namespace", + Namespace::Module => "namespace", + }; + let mut diagnostic = Diagnostic::error(format!( + "duplicate declaration `{name}` in {namespace_text}" + )) + .with_code(DiagnosticCode::NAMERES_DUPLICATE_DECLARATION) + .with_primary_label_span(span.clone(), Some("duplicate declaration")) + .with_secondary_label_span(previous.clone(), Some("previous declaration")); + if let Some(context) = context { + diagnostic = diagnostic.with_note(format!("context: {context}")); + } + diagnostic + } + } + } +} + +fn replace_with_suggestion( + span: &LabelSpan, + replacement: &str, + applicability: Applicability, +) -> Suggestion { + Suggestion { + title: format!("Replace with `{replacement}`"), + applicability, + edits: vec![AnchoredTextEdit { + span: span.clone(), + replacement: replacement.to_owned(), + }], + } +} + +fn replacement_for_name<'a>(name: &str, suggestion: &'a str) -> Option<&'a str> { + match (name.rsplit_once('.'), suggestion.rsplit_once('.')) { + (Some((name_qualifier, _)), Some((suggestion_qualifier, leaf))) + if name_qualifier == suggestion_qualifier => + { + Some(leaf) + } + (Some(_), _) => None, + (None, _) => Some(suggestion), + } +} + +pub(super) fn duplicate_diagnostic<'db>( + db: &'db dyn Db, + namespace: Namespace, + name: &str, + span: Span<'db>, + previous: Span<'db>, + context: Option<&str>, +) -> NameresDiagnostic { + NameresDiagnostic::DuplicateDeclaration { + namespace, + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + previous: LabelSpan::from_span(db, previous), + context: context.map(ToOwned::to_owned), + } +} + +pub(super) fn undefined_name<'db>( + db: &'db dyn Db, + name: &str, + span: Span<'db>, + kind: UndefinedNameKind, + suggestion: Option, + private_candidate: Option, +) -> NameresDiagnostic { + NameresDiagnostic::UndefinedName { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + kind, + suggestion, + private_candidate, + } +} + +pub(super) fn undefined_type_ctor<'db>( + db: &'db dyn Db, + name: &str, + span: Span<'db>, + suggestion: Option, + constructor_candidate: Option, +) -> NameresDiagnostic { + NameresDiagnostic::UndefinedTypeConstructor { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + suggestion, + constructor_candidate, + } +} + +pub(super) fn undefined_class<'db>( + db: &'db dyn Db, + name: &str, + span: Span<'db>, +) -> NameresDiagnostic { + NameresDiagnostic::UndefinedClass { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn invalid_pattern<'db>(db: &'db dyn Db, span: Span<'db>) -> NameresDiagnostic { + NameresDiagnostic::InvalidPattern { + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn unqualified_constructor<'db>( + db: &'db dyn Db, + name: &str, + span: Span<'db>, + qualification: Option, +) -> NameresDiagnostic { + NameresDiagnostic::UnqualifiedConstructor { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + qualification, + } +} diff --git a/crates/hir/src/nameres/mod.rs b/crates/hir/src/nameres/mod.rs new file mode 100644 index 00000000..a264988f --- /dev/null +++ b/crates/hir/src/nameres/mod.rs @@ -0,0 +1,75 @@ +//! Intra-module name resolution. +//! +//! This resolver builds lexical item/body scopes for one lowered module and +//! records what every type reference, predicate, expression, statement binder, +//! and pattern binder resolves to. Inter-module imports are injected through +//! the `ImportedNames` trait; this crate remains responsible for local language +//! semantics and builtin lookup. +//! +//! Solcore has distinct type and term namespaces. Type aliases, data types, +//! contracts, classes, type variables, and builtin type/class names live in the +//! type namespace. Functions, constructors, class methods, parameters, locals, +//! fields, modules used as qualifiers, and builtin values/functions live in the +//! term/module lookup surface. Constructor leaves are intentionally not +//! accepted unqualified when they would be ambiguous with the type that owns +//! them; callers must use qualified constructor syntax. +//! +//! Body scoping follows the reference semantics: +//! - A `let` initializer is resolved before the `let` binder is inserted, so +//! the initializer cannot refer to the binding being declared. +//! - `for` statements do not introduce their own lexical scope; their +//! initializer, condition, post statements, and body share the surrounding +//! scope. +//! - Inside a contract, fields beat same-name functions for bare references, + +use rustc_hash::{FxHashMap, FxHashSet}; +use tracing::{Level, field}; + +use crate::{ + Db, + anchor::DefId, + arena::Id, + ast::{ + Ident, + function::{ + Expr, ExprKind, FuncBody, FuncParam, FuncSig, FunctionVisibility, MatchArm, Pat, + PatCtorHead, PatKind, Stmt, StmtKind, + }, + item::{ + AdtDef, ClassDef, ContractDef, ContractItem, ContractKind, FieldDef, FunctionDef, + InstanceDef, Item, Module, TypeAlias, TypeAliasKind, + }, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + diag::{AnchoredTextEdit, Applicability, Diagnostic, DiagnosticCode, LabelSpan, Suggestion}, + span::{Span, Spanned, SpannedElem}, +}; + +mod body_resolver; +mod builtins; +mod diagnostic; +mod model; +mod queries; +mod scope; +mod type_resolver; +mod util; + +use body_resolver::BodyResolver; +use builtins::{best_name_suggestion, builtin_term, builtin_type_or_class}; +pub use diagnostic::{NameresDiagnostic, UndefinedNameKind}; +use diagnostic::{ + duplicate_diagnostic, invalid_pattern, undefined_class, undefined_name, undefined_type_ctor, + unqualified_constructor, +}; +pub use model::*; +pub use queries::*; +use scope::ItemScopeBuilder; +use type_resolver::TypeResolver; +use util::{ + collect_constructor_type_candidates, expr_path, ident_text_str, param_name, path_span, qualify, + record_body_fields, record_module_fields, unique_constructor_type_candidate, +}; +pub use util::{ + ident_text, is_direct_call_resolution, param_bindings, type_var_bindings, + type_var_bindings_from, +}; diff --git a/crates/hir/src/nameres/model.rs b/crates/hir/src/nameres/model.rs new file mode 100644 index 00000000..657b4cab --- /dev/null +++ b/crates/hir/src/nameres/model.rs @@ -0,0 +1,995 @@ +use super::*; + +const COMPILER_CONTRACT_METHOD_REF_PREFIX: &str = "$solcore$contract-method$"; + +/// Builds the reserved HIR-only spelling used to refer to a contract method +/// from compiler-generated bodies. +/// +/// The lexer cannot produce this identifier from source, so it cannot collide +/// with a field or with a qualified constructor such as `Contract.Method`. +pub fn compiler_contract_method_ref(name: &str) -> String { + format!("{COMPILER_CONTRACT_METHOD_REF_PREFIX}{name}") +} + +fn compiler_contract_method_name(name: &str) -> Option<&str> { + name.strip_prefix(COMPILER_CONTRACT_METHOD_REF_PREFIX) +} + +fn index_from_usize(value: usize) -> u32 { + u32::try_from(value).expect("name-resolution index exceeds u32::MAX") +} + +/// Name-resolution namespace. +/// +/// Type and term are the language namespaces. Field and module are represented +/// separately so diagnostics and import integration can distinguish lookup +/// surfaces that are not duplicate-checked like ordinary declarations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum Namespace { + /// Type-level names: aliases, ADTs, contracts, classes, type variables. + Type, + /// Term-level names: functions, constructors, locals, parameters, methods. + Term, + /// Contract field names. + Field, + /// Imported module binding names. + Module, +} + +/// Visible candidate for a constructor leaf. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ConstructorTypeCandidate { + /// Type that owns the constructor. + pub ty_name: String, + /// Constructor leaf name. + pub ctor_name: String, + /// Span of the constructor declaration. + pub span: LabelSpan, +} + +/// Private imported item found while resolving a qualified module access. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct PrivateCandidate { + /// Private item name. + pub name: String, + /// Module that declares the private item. + pub module: String, + /// Span of the private declaration. + pub span: LabelSpan, +} + +/// Kind of user definition reached by a resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum DefResolutionKind { + /// Function, method, constructor, or fallback definition. + Function, + /// Contract definition. + Contract, + /// Algebraic data type definition. + Adt, + /// Type alias definition. + TypeAlias, + /// Nominal user-defined value type definition. + ValueType, + /// Type class definition. + Class, + /// Type class instance definition. + Instance, +} + +/// Stable reference to a contract field. +/// +/// Fields are identified by their owning contract definition and declaration +/// index, which is stable under unrelated edits inside the contract body. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct FieldIndex(u32); + +impl FieldIndex { + pub const fn from_u32(v: u32) -> Self { + Self(v) + } + + pub const fn as_u32(self) -> u32 { + self.0 + } + + /// Creates a field index from a platform-sized index. + /// + /// # Panics + /// + /// Panics if `v` exceeds `u32::MAX`. + pub fn from_usize(v: usize) -> Self { + Self(index_from_usize(v)) + } + + pub const fn as_usize(self) -> usize { + self.0 as usize + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct FieldId<'db> { + /// Owning contract definition. + pub contract: DefId<'db>, + /// Zero-based field declaration index. + pub index: FieldIndex, +} + +/// Logical module binding visible in an item scope. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleRef<'db> { + /// Module definition that owns the binding. + pub owner: DefId<'db>, + /// Surface name used as the module qualifier. + pub name: String, +} + +/// Stable reference to a type variable binder. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct TypeVarId<'db> { + /// Definition that owns the type variable list. + pub owner: DefId<'db>, + /// Zero-based binder index in the owner. + pub index: u32, + /// Binder name. + pub name: String, +} + +/// Stable reference to a function-body parameter. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct ParamIndex(u32); + +impl ParamIndex { + pub const fn from_u32(v: u32) -> Self { + Self(v) + } + + pub const fn as_u32(self) -> u32 { + self.0 + } + + /// Creates a parameter index from a platform-sized index. + /// + /// # Panics + /// + /// Panics if `v` exceeds `u32::MAX`. + pub fn from_usize(v: usize) -> Self { + Self(index_from_usize(v)) + } + + pub const fn as_usize(self) -> usize { + self.0 as usize + } +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct CtorIndex(u32); + +impl CtorIndex { + pub const fn from_u32(v: u32) -> Self { + Self(v) + } + + pub const fn as_u32(self) -> u32 { + self.0 + } + + /// Creates a constructor index from a platform-sized index. + /// + /// # Panics + /// + /// Panics if `v` exceeds `u32::MAX`. + pub fn from_usize(v: usize) -> Self { + Self(index_from_usize(v)) + } + + pub const fn as_usize(self) -> usize { + self.0 as usize + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct ParamId<'db> { + /// Body whose parameter list introduced this parameter. + pub body: FuncBody<'db>, + /// Zero-based parameter index. + pub index: ParamIndex, +} + +/// Local binding introduced inside a body or type binder list. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum LocalBinding<'db> { + /// Binding introduced by a `let` statement. + Let { + /// Body containing the statement. + body: FuncBody<'db>, + /// Statement ID that introduced the binding. + stmt: Id>, + }, + /// Binding introduced by a pattern. + Pattern { + /// Body containing the pattern. + body: FuncBody<'db>, + /// Pattern ID that introduced the binding. + pat: Id>, + }, + /// Type variable binding. + TypeVar(TypeVarId<'db>), +} + +/// Builtin type names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinType { + /// `word`. + Word, + /// `bool`. + Bool, + /// `string`. + String, + /// Unit type `()`. + Unit, + /// Binary product type constructor. + Pair, + /// Binary sum type constructor. + Sum, + /// Integer type. + Integer, +} + +/// Builtin class names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinClass { + /// `invokable`. + Invokable, + /// `Int`. + Int, +} + +/// Builtin constructor names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinCtor { + /// Boolean `true`. + True, + /// Boolean `false`. + False, + /// Unit constructor `()`. + Unit, + /// Pair constructor. + Pair, + /// Sum left constructor. + Inl, + /// Sum right constructor. + Inr, +} + +/// Builtin function names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinFunction { + /// `invoke`. + Invoke, + /// Primitive word addition. + PrimAddWord, + /// Primitive word equality. + PrimEqWord, + /// Conversion from word to integer. + WordToInteger, + /// Conversion from integer to word. + WordFromInteger, + /// Integer addition. + IntegerAdd, + /// Integer subtraction. + IntegerSub, + /// Integer multiplication. + IntegerMul, + /// Integer less-than comparison. + IntegerLt, + /// Integer equality. + IntegerEq, +} + +/// Builtin class method names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinClassMethod { + /// `invokable.invoke`. + InvokableInvoke, + /// `Int.fromInteger`. + IntFromInteger, +} + +/// Builtin resolution category. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinKind { + /// Builtin type. + Type(BuiltinType), + /// Builtin class. + Class(BuiltinClass), + /// Builtin constructor. + Constructor(BuiltinCtor), + /// Builtin function. + Function(BuiltinFunction), + /// Builtin class method. + ClassMethod(BuiltinClassMethod), +} + +/// Result of resolving a name occurrence or binder. +/// +/// `Err` records that resolution failed, or that parser/import recovery made +/// the target intentionally unknown and diagnostics were suppressed at the +/// caller boundary. +/// `DotCtorDeferred` is used for leading-dot constructor syntax whose concrete +/// type is determined later by type information. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub enum Resolution<'db> { + /// User definition. + Def { + /// Definition identity. + def: DefId<'db>, + /// Definition category. + kind: DefResolutionKind, + }, + /// Local binding. + Local(LocalBinding<'db>), + /// Function or lambda parameter. + Param(ParamId<'db>), + /// Contract field. + Field(FieldId<'db>), + /// Data constructor. + Ctor { + /// Owning data type. + ty: DefId<'db>, + /// Constructor index in the owning data type. + index: CtorIndex, + }, + /// Type class method. + ClassMethod { + /// Owning class. + class: DefId<'db>, + /// Method name. + name: String, + }, + /// Module qualifier. + Module(ModuleRef<'db>), + /// Leading-dot constructor lookup deferred to type checking. + DotCtorDeferred, + /// Builtin item. + Builtin(BuiltinKind), + /// Failed resolution after diagnostics. + Err, +} + +/// Name exported by an item or imported scope. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ScopeEntry<'db> { + /// Surface name in the relevant namespace. + pub name: String, + /// Span of the declaration or imported binding. + pub span: Span<'db>, + /// Resolution reached by the name. + pub resolution: Resolution<'db>, +} + +/// Ordered namespace entries with an indexed first-name lookup. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update, Default)] +pub struct NamespaceTable<'db> { + entries: Vec>, + index: std::collections::BTreeMap, +} + +impl<'db> NamespaceTable<'db> { + /// Appends `entry` and records the first entry for its name. + pub fn push(&mut self, entry: ScopeEntry<'db>) { + let index = + u32::try_from(self.entries.len()).expect("namespace table entry count exceeds u32"); + self.index.entry(entry.name.clone()).or_insert(index); + self.entries.push(entry); + } + + /// Returns the first entry for `name`. + pub fn get(&self, name: &str) -> Option<&ScopeEntry<'db>> { + self.index + .get(name) + .and_then(|index| self.entries.get(*index as usize)) + } + + /// Iterates entries in insertion order. + pub fn iter(&self) -> std::slice::Iter<'_, ScopeEntry<'db>> { + self.entries.iter() + } +} + +impl<'a, 'db> IntoIterator for &'a NamespaceTable<'db> { + type Item = &'a ScopeEntry<'db>; + type IntoIter = std::slice::Iter<'a, ScopeEntry<'db>>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Constructor entry in a type's constructor list. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct CtorEntry<'db> { + /// Unqualified constructor leaf name. + pub name: String, + /// Qualified constructor name, usually `Type.Ctor`. + pub qualified_name: String, + /// Span of the constructor declaration. + pub span: Span<'db>, + /// Owning data type. + pub ty: DefId<'db>, + /// Constructor index in declaration order. + pub index: CtorIndex, +} + +/// Constructors associated with one data type. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct CtorList<'db> { + /// Owning data type. + pub ty: DefId<'db>, + /// Type name used for qualification. + pub ty_name: String, + /// Constructor entries in declaration order. + pub ctors: Vec>, +} + +/// Contract field entry. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct FieldEntry<'db> { + /// Field name. + pub name: String, + /// Span of the field declaration. + pub span: Span<'db>, + /// Stable field identity. + pub field: FieldId<'db>, +} + +/// Name scope contributed by a contract body. +/// +/// Contract scopes are nested below the module scope. They contain +/// contract-local types, terms, fields, and constructors, and are consulted +/// when resolving code inside that contract. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ContractScope<'db> { + /// Contract definition that owns this scope. + pub contract: DefId<'db>, + /// Contract name. + pub name: String, + /// Contract-local type entries. + pub types: NamespaceTable<'db>, + /// Contract-local term entries. + pub terms: NamespaceTable<'db>, + /// Function definitions declared `external`, which are not callable by a + /// bare name from inside the same declaration. + pub external_functions: Vec>, + /// Field entries. + pub fields: Vec>, + /// Constructor lists declared inside the contract. + pub ctor_lists: Vec>, +} + +/// Diagnostic side of an item-level scope. +pub type ItemScopeDiagnostics = Vec; + +/// Item-level lookup facts for one module. +/// +/// The scope records declarations before body resolution so functions can refer +/// to later items in the same module. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ItemScopeFacts<'db> { + /// Module this scope belongs to. + pub module: Module<'db>, + /// Type namespace entries. + pub types: NamespaceTable<'db>, + /// Term namespace entries. + pub terms: NamespaceTable<'db>, + /// Module qualifier entries introduced by imports. + pub modules: NamespaceTable<'db>, + /// Top-level constructor lists. + pub ctor_lists: Vec>, + /// Contract-local scopes. + pub contracts: Vec>, + /// Instance definitions in source order. + pub instances: Vec>, +} + +/// Item-level scope for one module. +/// +/// This is the compatibility composite used by diagnostic paths. Facts-only +/// consumers should depend on [`ItemScopeFacts`] so diagnostic changes do not +/// invalidate downstream type work. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ItemScope<'db> { + /// Lookup facts for item and body resolution. + pub facts: ItemScopeFacts<'db>, + /// Diagnostics found while building item scopes. + pub diagnostics: ItemScopeDiagnostics, +} + +/// Resolution attached to an unresolved type reference. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct TypeResolution<'db> { + /// Type reference being resolved. + pub ty: TypeRef<'db>, + /// Resolution for the named constructor or `Err`. + pub resolution: Resolution<'db>, +} + +/// Resolution attached to an unresolved predicate reference. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct PredResolution<'db> { + /// Predicate being resolved. + pub pred: PredRef<'db>, + /// Resolution for the class name or `Err`. + pub resolution: Resolution<'db>, +} + +/// Diagnostic side of item-signature resolution. +pub type ItemResolutionDiagnostics = Vec; + +/// Type and predicate resolution facts for item signatures. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update, Default)] +pub struct ItemResolutionFacts<'db> { + /// Resolved type references. + pub types: Vec>, + /// Resolved predicate references. + pub preds: Vec>, +} + +/// Type and predicate resolutions for item signatures. +/// +/// This compatibility composite preserves diagnostics for callers that publish +/// nameres output. Facts-only consumers should depend on +/// [`ItemResolutionFacts`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update, Default)] +pub struct ItemResolutionMap<'db> { + /// Resolution facts used by type lowering and inference. + pub facts: ItemResolutionFacts<'db>, + /// Diagnostics found while resolving item signatures. + pub diagnostics: ItemResolutionDiagnostics, +} + +/// Resolution attached to an expression occurrence. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyExprResolution<'db> { + /// Body containing the expression. + pub body: FuncBody<'db>, + /// Expression ID in the body arena. + pub expr: Id>, + /// Resolved expression name or sentinel. + pub resolution: Resolution<'db>, +} + +/// Resolution attached to a statement binder. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyStmtResolution<'db> { + /// Body containing the statement. + pub body: FuncBody<'db>, + /// Statement ID that introduced the binder. + pub stmt: Id>, + /// Local binding resolution for the statement. + pub resolution: Resolution<'db>, +} + +/// Resolution attached to a pattern binder or constructor occurrence. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyPatResolution<'db> { + /// Body containing the pattern. + pub body: FuncBody<'db>, + /// Pattern ID in the body arena. + pub pat: Id>, + /// Pattern resolution. + pub resolution: Resolution<'db>, +} + +/// Name-resolution results for one function body. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update, Default)] +pub struct BodyResolutionMap<'db> { + /// Expression resolutions. + pub exprs: Vec>, + /// Statement binder resolutions. + pub stmt_bindings: Vec>, + /// Pattern resolutions. + pub pats: Vec>, + /// Type references used in the body. + pub types: Vec>, + /// Predicate references used in the body. + pub preds: Vec>, + /// Diagnostics found while resolving this body. + pub diagnostics: Vec, +} + +/// Parameter binding passed into body resolution. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ParamBinding<'db> { + /// Parameter name with source span. + pub name: SpannedElem<'db, Ident<'db>>, +} + +/// Type-variable binding passed into body or item resolution. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct TypeVarBinding<'db> { + /// Definition that owns the type variable list. + pub owner: DefId<'db>, + /// Type variable name with source span. + pub name: SpannedElem<'db, Ident<'db>>, + /// Zero-based binder index. + pub index: u32, +} + +/// Context required to resolve a function body. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct BodyResolutionContext<'db> { + /// Module containing the body. + pub module: Module<'db>, + /// Contract enclosing the body, if any. + pub enclosing_contract: Option>, + /// Parameters visible at body entry. + pub params: Vec>, + /// Type variables visible at body entry. + pub type_vars: Vec>, +} + +/// Complete local resolution result for one module. +#[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleResolutionMap<'db> { + /// Item-level scope built for the module. + pub item_scope: ItemScope<'db>, + /// Type and predicate resolutions in item signatures. + pub item_resolutions: ItemResolutionMap<'db>, + /// Body resolution maps for functions and methods. + pub bodies: Vec>, + /// Diagnostics found while resolving this module. + pub diagnostics: Vec, +} + +/// Diagnostic emission policy for name resolution. +/// +/// Parser recovery can leave `Error` HIR nodes and can also lose declarations. +/// When a source file already has parse diagnostics, callers should still build +/// resolution maps for editor features, but must suppress all nameres +/// diagnostics. This matches the reference behavior of stopping after parse +/// errors and avoids showing cascades from an incomplete recovered HIR. We also +/// suppress `SC0108` duplicate diagnostics in this mode because recovery can +/// distort item boundaries, so even structure-like checks are not guaranteed to +/// be sound. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NameresDiagnosticPolicy { + /// Emit name-resolution diagnostics normally. + Emit, + /// Keep resolution data but clear all name-resolution diagnostics. + SuppressForParseErrors, +} + +impl NameresDiagnosticPolicy { + fn suppresses_diagnostics(self) -> bool { + matches!(self, Self::SuppressForParseErrors) + } +} + +/// Provider of names imported from other modules. +/// +/// HIR name resolution is parameterized by this trait so the inter-module +/// resolver can inject imported items without making `hir` depend on the module +/// graph crate. +pub trait ImportedNames<'db> { + /// Looks up an imported name in `namespace`. + fn imported( + &self, + db: &'db dyn Db, + namespace: Namespace, + name: &str, + ) -> Option>; + + /// Returns whether any imported constructor has the given unqualified leaf. + /// + /// The default is `false` so purely local resolution can ignore import + /// constructor ambiguity. + fn has_constructor_leaf(&self, _db: &'db dyn Db, _leaf: &str) -> bool { + false + } + + /// Returns whether an imported parse-broken module may still contain this + /// unqualified name. + /// + /// Import providers with parse errors have an incomplete public interface: + /// absence from the recovered interface is not evidence that a name is + /// truly missing. Returning `true` lets HIR resolution produce + /// [`Resolution::Err`] without an undefined-name diagnostic. + fn may_contain_unknown_unqualified( + &self, + _db: &'db dyn Db, + _namespace: Namespace, + _name: &str, + ) -> bool { + false + } + + /// Returns whether a module qualifier targets a parse-broken provider whose + /// members are therefore unknown. + fn has_incomplete_module_qualifier(&self, _db: &'db dyn Db, _qualifier: &str) -> bool { + false + } + + /// Returns imported names that are visible in `namespace`. + fn candidate_names(&self, _db: &'db dyn Db, _namespace: Namespace) -> Vec { + Vec::new() + } + + /// Returns visible constructor/type pairs with the given constructor leaf. + fn constructor_type_candidates( + &self, + _db: &'db dyn Db, + _leaf: &str, + ) -> Vec { + Vec::new() + } + + /// Returns an exact private item behind a qualified module access, when the + /// provider can prove the item exists but is not exported. + fn private_candidate( + &self, + _db: &'db dyn Db, + _namespace: Namespace, + _qualifier: &str, + _name: &str, + ) -> Option { + None + } +} + +/// Empty import provider used by standalone HIR queries. +#[derive(Debug, Clone, Copy)] +pub struct EmptyImportedNames; + +impl<'db> ImportedNames<'db> for EmptyImportedNames { + fn imported( + &self, + _db: &'db dyn Db, + _namespace: Namespace, + _name: &str, + ) -> Option> { + None + } +} + +impl<'db> std::ops::Deref for ItemScope<'db> { + type Target = ItemScopeFacts<'db>; + + fn deref(&self) -> &Self::Target { + &self.facts + } +} + +impl<'db> std::ops::DerefMut for ItemScope<'db> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.facts + } +} + +impl<'db> std::ops::Deref for ItemResolutionMap<'db> { + type Target = ItemResolutionFacts<'db>; + + fn deref(&self) -> &Self::Target { + &self.facts + } +} + +impl<'db> std::ops::DerefMut for ItemResolutionMap<'db> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.facts + } +} + +impl<'db> ItemScope<'db> { + /// Returns the lookup facts without diagnostics. + pub fn facts(&self) -> ItemScopeFacts<'db> { + self.facts.clone() + } +} + +impl<'db> ItemResolutionMap<'db> { + /// Returns the resolution facts without diagnostics. + pub fn facts(&self) -> ItemResolutionFacts<'db> { + self.facts.clone() + } +} + +impl<'db> ItemScopeFacts<'db> { + /// Resolves a type name declared in this module scope. + pub fn type_resolution(&self, name: &str) -> Option> { + self.types.get(name).map(|entry| entry.resolution.clone()) + } + + /// Resolves a term name declared in this module scope. + pub fn term_resolution(&self, name: &str) -> Option> { + self.terms.get(name).map(|entry| entry.resolution.clone()) + } + + /// Resolves a module qualifier name introduced by imports. + pub fn module_resolution(&self, name: &str) -> Option> { + self.modules.get(name).map(|entry| entry.resolution.clone()) + } + + /// Returns the contract-local scope for `contract`. + pub fn contract_scope(&self, contract: DefId<'db>) -> Option<&ContractScope<'db>> { + self.contracts + .iter() + .find(|scope| scope.contract == contract) + } + + /// Returns whether any visible constructor has the given leaf name. + /// + /// This powers diagnostics for unqualified constructor use and does not + /// resolve to a concrete constructor by itself. + pub fn has_constructor_leaf(&self, leaf: &str) -> bool { + self.ctor_lists + .iter() + .flat_map(|list| &list.ctors) + .any(|ctor| ctor.name == leaf) + || self + .contracts + .iter() + .flat_map(|scope| &scope.ctor_lists) + .flat_map(|list| &list.ctors) + .any(|ctor| ctor.name == leaf) + } +} + +impl<'db> ContractScope<'db> { + pub(super) fn type_resolution(&self, name: &str) -> Option> { + self.types.get(name).map(|entry| entry.resolution.clone()) + } + + pub(super) fn term_resolution(&self, name: &str) -> Option> { + // Constructor terms for a contract-local ADT can already have a key + // such as `Option.Some`, even when the enclosing contract is also + // named `Option`. Preserve exact source lookup, and recognize the + // unspellable compiler-only method reference independently. + if let Some(entry) = compiler_contract_method_name(name) + .and_then(|method| self.terms.get(method)) + .filter(|entry| { + matches!( + entry.resolution, + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } + ) + }) + { + return Some(entry.resolution.clone()); + } + self.terms + .get(name) + .filter(|entry| self.is_unqualified_term_visible(entry)) + .map(|entry| entry.resolution.clone()) + } + + /// Returns whether `entry` can be referenced by a bare name inside the + /// declaration that owns this scope. + pub fn is_unqualified_term_visible(&self, entry: &ScopeEntry<'db>) -> bool { + !self.is_external_function_resolution(&entry.resolution) + } + + /// Returns whether `resolution` names an `external` function declared by + /// the declaration that owns this scope. + pub fn is_external_function_resolution(&self, resolution: &Resolution<'db>) -> bool { + matches!( + resolution, + Resolution::Def { + def, + kind: DefResolutionKind::Function, + } if self.external_functions.contains(&def) + ) + } + + pub(super) fn field_resolution(&self, name: &str) -> Option> { + self.fields + .iter() + .find(|entry| entry.name == name) + .map(|entry| Resolution::Field(entry.field)) + } + + pub(super) fn has_constructor_leaf(&self, leaf: &str) -> bool { + self.ctor_lists + .iter() + .flat_map(|list| &list.ctors) + .any(|ctor| ctor.name == leaf) + } +} + +impl<'db> BodyResolutionMap<'db> { + pub(super) fn record_expr( + &mut self, + body: FuncBody<'db>, + expr: Id>, + resolution: Resolution<'db>, + ) { + self.exprs.push(BodyExprResolution { + body, + expr, + resolution, + }); + } + + pub(super) fn record_stmt( + &mut self, + body: FuncBody<'db>, + stmt: Id>, + resolution: Resolution<'db>, + ) { + self.stmt_bindings.push(BodyStmtResolution { + body, + stmt, + resolution, + }); + } + + pub(super) fn record_pat( + &mut self, + body: FuncBody<'db>, + pat: Id>, + resolution: Resolution<'db>, + ) { + self.pats.push(BodyPatResolution { + body, + pat, + resolution, + }); + } +} + +impl<'db> ItemResolutionMap<'db> { + pub(super) fn apply_diagnostic_policy(&mut self, policy: NameresDiagnosticPolicy) { + if policy.suppresses_diagnostics() { + self.diagnostics.clear(); + } + } +} + +impl<'db> BodyResolutionMap<'db> { + pub(super) fn apply_diagnostic_policy(&mut self, policy: NameresDiagnosticPolicy) { + if policy.suppresses_diagnostics() { + self.diagnostics.clear(); + } + } +} + +impl<'db> ModuleResolutionMap<'db> { + pub(super) fn apply_diagnostic_policy(&mut self, policy: NameresDiagnosticPolicy) { + if !policy.suppresses_diagnostics() { + return; + } + self.item_scope.diagnostics.clear(); + self.item_resolutions.apply_diagnostic_policy(policy); + for body in &mut self.bodies { + body.apply_diagnostic_policy(policy); + } + self.diagnostics.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::{CtorIndex, FieldIndex, ParamIndex}; + + #[test] + fn index_types_preserve_representable_usize_values() { + for value in [0, 42, u32::MAX as usize] { + assert_eq!(FieldIndex::from_usize(value).as_usize(), value); + assert_eq!(ParamIndex::from_usize(value).as_usize(), value); + assert_eq!(CtorIndex::from_usize(value).as_usize(), value); + } + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn index_types_reject_usize_values_larger_than_u32() { + assert!(std::panic::catch_unwind(|| FieldIndex::from_usize(usize::MAX)).is_err()); + assert!(std::panic::catch_unwind(|| ParamIndex::from_usize(usize::MAX)).is_err()); + assert!(std::panic::catch_unwind(|| CtorIndex::from_usize(usize::MAX)).is_err()); + } +} diff --git a/crates/hir/src/nameres/queries.rs b/crates/hir/src/nameres/queries.rs new file mode 100644 index 00000000..e966e5b0 --- /dev/null +++ b/crates/hir/src/nameres/queries.rs @@ -0,0 +1,322 @@ +use super::*; + +/// Builds the item-level scope for `module`. +/// +/// This query collects declarations before resolving bodies so forward +/// references between top-level items are legal. It also emits duplicate-name +/// diagnostics for the type and term namespaces. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, module), + fields(file = field::Empty, def = field::Empty) +)] +pub fn item_scope<'db>(db: &'db dyn Db, module: Module<'db>) -> ItemScope<'db> { + record_module_fields(db, module); + let mut builder = ItemScopeBuilder::new(db, module); + for item in module.items(db) { + builder.add_item(*item); + } + builder.finish() +} + +/// Returns item-level lookup facts without duplicate-name diagnostics. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, module), + fields(file = field::Empty, def = field::Empty) +)] +pub fn item_scope_facts<'db>(db: &'db dyn Db, module: Module<'db>) -> ItemScopeFacts<'db> { + record_module_fields(db, module); + item_scope(db, module).facts() +} + +/// Resolves type and predicate references in item signatures without imports. +/// +/// This is the standalone HIR query. Inter-module callers should use +/// [`resolve_item_types_with_imports`] so imported names participate in lookup. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, module), + fields(file = field::Empty, def = field::Empty) +)] +pub fn resolve_item_types<'db>(db: &'db dyn Db, module: Module<'db>) -> ItemResolutionMap<'db> { + record_module_fields(db, module); + let scope = item_scope(db, module); + let imports = EmptyImportedNames; + resolve_item_types_with_imports(db, module, &scope, &imports) +} + +/// Resolves item-signature type and predicate facts without diagnostics. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, module), + fields(file = field::Empty, def = field::Empty) +)] +pub fn resolve_item_type_facts<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> ItemResolutionFacts<'db> { + record_module_fields(db, module); + let scope = item_scope_facts(db, module); + let imports = EmptyImportedNames; + resolve_item_type_facts_with_imports(db, module, &scope, &imports) +} + +/// Resolves type and predicate references in item signatures with imported +/// names. +/// +/// `scope` must be the item scope for `module`. `imports` is consulted after +/// local item/contract scopes and before builtin names. +pub fn resolve_item_types_with_imports<'db>( + db: &'db dyn Db, + module: Module<'db>, + scope: &ItemScopeFacts<'db>, + imports: &dyn ImportedNames<'db>, +) -> ItemResolutionMap<'db> { + let mut resolver = TypeResolver::new(db, scope, imports); + for item in module.items(db) { + resolver.item(*item, None, &[]); + } + resolver.map +} + +/// Resolves type and predicate references in item signatures with imported +/// names and returns only lookup facts. +pub fn resolve_item_type_facts_with_imports<'db>( + db: &'db dyn Db, + module: Module<'db>, + scope: &ItemScopeFacts<'db>, + imports: &dyn ImportedNames<'db>, +) -> ItemResolutionFacts<'db> { + resolve_item_types_with_imports(db, module, scope, imports).facts() +} + +/// Resolves one function body without imported names. +/// +/// `context` supplies the module, optional enclosing contract, parameters, and +/// inherited type variables. The returned map is silent for parser `Error` +/// nodes; parse diagnostics are produced during lowering. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, body, context), + fields(file = field::Empty, def = field::Empty) +)] +pub fn resolve_body<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + context: BodyResolutionContext<'db>, +) -> BodyResolutionMap<'db> { + record_body_fields(db, body); + let imports = EmptyImportedNames; + resolve_body_with_imports(db, body, &context, &imports) +} + +/// Resolves one function body with imported names. +/// +/// This entry point is used by the inter-module resolver. It preserves the +/// local scoping rules documented at module level and consults `imports` only +/// after local/field/item lookup has failed. +pub fn resolve_body_with_imports<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + context: &BodyResolutionContext<'db>, + imports: &dyn ImportedNames<'db>, +) -> BodyResolutionMap<'db> { + resolve_body_with_imports_and_policy(db, body, context, imports, NameresDiagnosticPolicy::Emit) +} + +/// Resolves one function body with imported names and an explicit diagnostic +/// policy. +pub fn resolve_body_with_imports_and_policy<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + context: &BodyResolutionContext<'db>, + imports: &dyn ImportedNames<'db>, + policy: NameresDiagnosticPolicy, +) -> BodyResolutionMap<'db> { + let scope = item_scope_facts(db, context.module); + let mut resolver = BodyResolver::new(db, &scope, imports, context.enclosing_contract); + resolver.with_type_vars(&context.type_vars, |resolver| { + resolver.with_scope(|resolver| { + for (index, param) in context.params.iter().enumerate() { + resolver.add_param(body, index, ¶m.name); + } + resolver.body(body); + }); + }); + let mut map = resolver.map; + map.apply_diagnostic_policy(policy); + map +} + +/// Resolves all item signatures and function bodies in a module without +/// imports. +#[salsa::tracked] +#[tracing::instrument( + target = "hir::query", + level = "debug", + skip(db, module), + fields(file = field::Empty, def = field::Empty) +)] +pub fn resolve_module<'db>(db: &'db dyn Db, module: Module<'db>) -> ModuleResolutionMap<'db> { + record_module_fields(db, module); + let scope = item_scope(db, module); + let imports = EmptyImportedNames; + resolve_module_with_imports(db, module, scope, &imports) +} + +/// Resolves all item signatures and function bodies in a module with imports. +/// +/// The supplied `scope` is reused for both item and body resolution so +/// duplicate diagnostics and lookup surfaces are computed once. +pub fn resolve_module_with_imports<'db>( + db: &'db dyn Db, + module: Module<'db>, + scope: ItemScope<'db>, + imports: &dyn ImportedNames<'db>, +) -> ModuleResolutionMap<'db> { + resolve_module_with_imports_and_policy( + db, + module, + scope, + imports, + NameresDiagnosticPolicy::Emit, + ) +} + +/// Resolves all item signatures and function bodies with an explicit diagnostic +/// policy. +pub fn resolve_module_with_imports_and_policy<'db>( + db: &'db dyn Db, + module: Module<'db>, + scope: ItemScope<'db>, + imports: &dyn ImportedNames<'db>, + policy: NameresDiagnosticPolicy, +) -> ModuleResolutionMap<'db> { + let item_resolutions = resolve_item_types_with_imports(db, module, &scope, imports); + let mut bodies = Vec::new(); + for item in module.items(db) { + collect_item_body_resolutions(db, module, *item, None, &[], imports, &mut bodies); + } + let mut diagnostics = scope.diagnostics.clone(); + diagnostics.extend(item_resolutions.diagnostics.iter().cloned()); + for body in &bodies { + diagnostics.extend(body.diagnostics.iter().cloned()); + } + let mut map = ModuleResolutionMap { + item_scope: scope, + item_resolutions, + bodies, + diagnostics, + }; + map.apply_diagnostic_policy(policy); + map +} + +fn collect_item_body_resolutions<'db>( + db: &'db dyn Db, + module: Module<'db>, + item: Item<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[TypeVarBinding<'db>], + imports: &dyn ImportedNames<'db>, + bodies: &mut Vec>, +) { + match item { + Item::FunctionDef(def) => { + collect_function_body_resolution( + db, + module, + def, + enclosing_contract.map(|contract| contract.def_id_value(db)), + inherited_type_vars, + imports, + bodies, + ); + } + Item::InstanceDef(def) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + def.def_id_value(db), + def.type_var_elems(db), + )); + for method in def.methods(db) { + collect_function_body_resolution( + db, + module, + *method, + enclosing_contract.map(|contract| contract.def_id_value(db)), + &inherited, + imports, + bodies, + ); + } + } + Item::ContractDef(def) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + def.def_id_value(db), + def.ty_param_elems(db), + )); + for item in def.items(db) { + match *item { + ContractItem::FunctionDef(defn) => { + collect_function_body_resolution( + db, + module, + defn, + Some(def.def_id_value(db)), + &inherited, + imports, + bodies, + ); + } + ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +fn collect_function_body_resolution<'db>( + db: &'db dyn Db, + module: Module<'db>, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[TypeVarBinding<'db>], + imports: &dyn ImportedNames<'db>, + bodies: &mut Vec>, +) { + let Some(body) = function.body(db) else { + return; + }; + let sig = function.sig(db); + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(type_var_bindings(function.def_id_value(db), &sig.type_vars)); + let context = BodyResolutionContext { + module, + enclosing_contract, + params: param_bindings(sig.params.atom()), + type_vars, + }; + bodies.push(resolve_body_with_imports(db, body, &context, imports)); +} diff --git a/crates/hir/src/nameres/scope.rs b/crates/hir/src/nameres/scope.rs new file mode 100644 index 00000000..e77df18a --- /dev/null +++ b/crates/hir/src/nameres/scope.rs @@ -0,0 +1,636 @@ +use super::*; + +pub(super) struct ItemScopeBuilder<'db> { + db: &'db dyn Db, + module: Module<'db>, + types: ScopeTableBuilder<'db>, + terms: ScopeTableBuilder<'db>, + modules: ScopeTableBuilder<'db>, + ctor_lists: Vec>, + contracts: Vec>, + instances: Vec>, + diagnostics: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TypeDeclFamily { + Alias, + Adt, + Class, + Contract, +} + +enum DuplicatePolicy<'a> { + SingleSpan { + context: Option<&'a str>, + }, + TypeFamilies { + family: TypeDeclFamily, + context: Option<&'a str>, + }, + Silent, +} + +enum DuplicateIndex<'db> { + SingleSpan { + namespace: Namespace, + names: FxHashMap>, + }, + TypeFamilies { + names: FxHashMap)>>, + }, + Silent, +} + +struct ScopeTableBuilder<'db> { + entries: Vec>, + duplicate_index: DuplicateIndex<'db>, +} + +impl<'db> ScopeTableBuilder<'db> { + fn single_span(namespace: Namespace) -> Self { + Self { + entries: Vec::new(), + duplicate_index: DuplicateIndex::SingleSpan { + namespace, + names: FxHashMap::default(), + }, + } + } + + fn type_families() -> Self { + Self { + entries: Vec::new(), + duplicate_index: DuplicateIndex::TypeFamilies { + names: FxHashMap::default(), + }, + } + } + + fn silent() -> Self { + Self { + entries: Vec::new(), + duplicate_index: DuplicateIndex::Silent, + } + } + + fn push( + &mut self, + db: &'db dyn Db, + diagnostics: &mut Vec, + policy: DuplicatePolicy<'_>, + entry: ScopeEntry<'db>, + ) { + self.check_duplicate(db, diagnostics, policy, &entry); + self.entries.push(entry); + } + + fn into_table(self) -> NamespaceTable<'db> { + let mut table = NamespaceTable::default(); + for entry in self.entries { + table.push(entry); + } + table + } + + fn contains_name(&self, name: &str) -> bool { + self.entries.iter().any(|entry| entry.name == name) + } + + fn check_duplicate( + &mut self, + db: &'db dyn Db, + diagnostics: &mut Vec, + policy: DuplicatePolicy<'_>, + entry: &ScopeEntry<'db>, + ) { + match policy { + DuplicatePolicy::SingleSpan { context } => { + let DuplicateIndex::SingleSpan { namespace, names } = &mut self.duplicate_index + else { + unreachable!("single-span duplicate policy used with incompatible scope table") + }; + if let Some(previous) = names.get(&entry.name).copied() { + diagnostics.push(duplicate_diagnostic( + db, + *namespace, + &entry.name, + entry.span, + previous, + context, + )); + } else { + names.insert(entry.name.clone(), entry.span); + } + } + DuplicatePolicy::TypeFamilies { family, context } => { + let DuplicateIndex::TypeFamilies { names } = &mut self.duplicate_index else { + unreachable!("type-family duplicate policy used with incompatible scope table") + }; + let previous = names.entry(entry.name.clone()).or_default(); + if let Some((_, previous_span)) = previous.iter().find(|(previous_family, _)| { + !type_decl_families_can_share(*previous_family, family) + }) { + diagnostics.push(duplicate_diagnostic( + db, + Namespace::Type, + &entry.name, + entry.span, + *previous_span, + context, + )); + } + previous.push((family, entry.span)); + } + DuplicatePolicy::Silent => {} + } + } +} + +impl<'db> ItemScopeBuilder<'db> { + pub(super) fn new(db: &'db dyn Db, module: Module<'db>) -> Self { + Self { + db, + module, + types: ScopeTableBuilder::type_families(), + terms: ScopeTableBuilder::single_span(Namespace::Term), + modules: ScopeTableBuilder::silent(), + ctor_lists: Vec::new(), + contracts: Vec::new(), + instances: Vec::new(), + diagnostics: Vec::new(), + } + } + + pub(super) fn finish(self) -> ItemScope<'db> { + ItemScope { + facts: ItemScopeFacts { + module: self.module, + types: self.types.into_table(), + terms: self.terms.into_table(), + modules: self.modules.into_table(), + ctor_lists: self.ctor_lists, + contracts: self.contracts, + instances: self.instances, + }, + diagnostics: self.diagnostics, + } + } + + pub(super) fn add_item(&mut self, item: Item<'db>) { + match item { + Item::FunctionDef(def) => self.add_function(def, None), + Item::TypeAlias(def) => self.add_alias(def, None), + Item::AdtDef(def) => self.add_adt(def, None), + Item::ClassDef(def) => self.add_class(def), + Item::InstanceDef(def) => self.instances.push(def), + Item::ContractDef(def) => self.add_contract(def), + Item::Import(def) if def.selector(self.db).is_none() => { + self.add_import_modules(def.path_elems(self.db), def.alias_elem(self.db)) + } + Item::Import(_) => {} + Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } + } + + fn add_type( + &mut self, + name: SpannedElem<'db, Ident<'db>>, + resolution: Resolution<'db>, + contract: Option<&mut ContractScopeBuilder<'db>>, + family: TypeDeclFamily, + ) { + let text = ident_text_str(self.db, &name).to_owned(); + if let Some(contract) = contract { + contract.add_type(text, name.span(self.db), resolution); + return; + } + let span = name.span(self.db); + self.types.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::TypeFamilies { + family, + context: None, + }, + ScopeEntry { + name: text, + span, + resolution, + }, + ); + } + + fn add_term( + &mut self, + name: String, + span: Span<'db>, + resolution: Resolution<'db>, + contract: Option<&mut ContractScopeBuilder<'db>>, + ) { + if let Some(contract) = contract { + contract.add_term(name, span, resolution); + return; + } + self.terms.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::SingleSpan { context: None }, + ScopeEntry { + name, + span, + resolution, + }, + ); + } + + fn add_silent_term( + &mut self, + name: String, + span: Span<'db>, + resolution: Resolution<'db>, + contract: Option<&mut ContractScopeBuilder<'db>>, + ) { + if let Some(contract) = contract { + contract.add_silent_term(name, span, resolution); + return; + } + self.terms.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::Silent, + ScopeEntry { + name, + span, + resolution, + }, + ); + } + + fn add_function( + &mut self, + def: FunctionDef<'db>, + mut contract: Option<&mut ContractScopeBuilder<'db>>, + ) { + let sig = def.sig(self.db); + if sig.visibility_kind() == Some(FunctionVisibility::External) + && let Some(contract) = contract.as_deref_mut() + { + contract.external_functions.push(def.def_id_value(self.db)); + } + self.add_term( + ident_text_str(self.db, &sig.name).to_owned(), + sig.name.span(self.db), + Resolution::Def { + def: def.def_id_value(self.db), + kind: DefResolutionKind::Function, + }, + contract, + ); + } + + fn add_alias(&mut self, def: TypeAlias<'db>, contract: Option<&mut ContractScopeBuilder<'db>>) { + let kind = match def.kind(self.db) { + TypeAliasKind::Transparent => DefResolutionKind::TypeAlias, + TypeAliasKind::ValueType => DefResolutionKind::ValueType, + }; + self.add_type( + def.name_elem(self.db), + Resolution::Def { + def: def.def_id_value(self.db), + kind, + }, + contract, + TypeDeclFamily::Alias, + ); + } + + fn add_adt(&mut self, def: AdtDef<'db>, mut contract: Option<&mut ContractScopeBuilder<'db>>) { + let ty_name = ident_text_str(self.db, &def.name_elem(self.db)).to_owned(); + let ty_def = def.def_id_value(self.db); + let mut ctor_entries = Vec::new(); + for ctor in def.ctors(self.db) { + let Some(field_names) = &ctor.field_names else { + continue; + }; + let mut names = FxHashMap::default(); + let context = format!("struct {ty_name}"); + for field in field_names { + let name = ident_text_str(self.db, field).to_owned(); + if let Some(previous) = names.get(&name).copied() { + self.diagnostics.push(duplicate_diagnostic( + self.db, + Namespace::Field, + &name, + field.span(self.db), + previous, + Some(&context), + )); + } else { + names.insert(name, field.span(self.db)); + } + } + } + self.add_type( + def.name_elem(self.db), + Resolution::Def { + def: ty_def, + kind: DefResolutionKind::Adt, + }, + contract.as_deref_mut(), + TypeDeclFamily::Adt, + ); + for (index, ctor) in def.ctors(self.db).iter().enumerate() { + let index = CtorIndex::from_usize(index); + let ctor_name = ident_text_str(self.db, &ctor.name).to_owned(); + let qualified = qualify(&ty_name, &ctor_name); + let entry = CtorEntry { + name: ctor_name, + qualified_name: qualified.clone(), + span: ctor.name.span(self.db), + ty: ty_def, + index, + }; + ctor_entries.push(entry); + self.add_term( + qualified, + ctor.name.span(self.db), + Resolution::Ctor { ty: ty_def, index }, + contract.as_deref_mut(), + ); + } + + let list = CtorList { + ty: ty_def, + ty_name, + ctors: ctor_entries, + }; + if let Some(contract) = contract { + contract.ctor_lists.push(list); + } else { + self.ctor_lists.push(list); + } + } + + fn add_class(&mut self, def: ClassDef<'db>) { + let head = def.head(self.db); + let class_name = head.kind(self.db).class; + let class_text = ident_text_str(self.db, &class_name).to_owned(); + self.add_type( + class_name, + Resolution::Def { + def: def.def_id_value(self.db), + kind: DefResolutionKind::Class, + }, + None, + TypeDeclFamily::Class, + ); + for method in def.methods(self.db) { + let method_name = ident_text_str(self.db, &method.name).to_owned(); + self.add_silent_term( + qualify(&class_text, &method_name), + method.name.span(self.db), + Resolution::ClassMethod { + class: def.def_id_value(self.db), + name: method_name, + }, + None, + ); + } + } + + fn add_contract(&mut self, def: ContractDef<'db>) { + let contract_name = ident_text_str(self.db, &def.name_elem(self.db)).to_owned(); + self.add_type( + def.name_elem(self.db), + Resolution::Def { + def: def.def_id_value(self.db), + kind: DefResolutionKind::Contract, + }, + None, + TypeDeclFamily::Contract, + ); + let mut contract = ContractScopeBuilder::new( + self.db, + def.def_id_value(self.db), + def.kind(self.db).keyword(), + contract_name, + ); + for (index, field) in def.fields(self.db).iter().enumerate() { + contract.add_field(field, index as u32); + } + for item in def.items(self.db) { + match *item { + ContractItem::FunctionDef(def) => self.add_function(def, Some(&mut contract)), + ContractItem::TypeAlias(def) => self.add_alias(def, Some(&mut contract)), + ContractItem::AdtDef(def) => self.add_adt(def, Some(&mut contract)), + ContractItem::Error { .. } => {} + } + } + let (contract_scope, diagnostics) = contract.finish(); + self.diagnostics.extend(diagnostics); + if def.kind(self.db) == ContractKind::Library { + let private_functions = def + .items(self.db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) + if function.sig(self.db).visibility_kind() + == Some(FunctionVisibility::Private) => + { + Some(function.def_id_value(self.db)) + } + _ => None, + }) + .collect::>(); + for entry in &contract_scope.types { + self.types.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::Silent, + ScopeEntry { + name: qualify(&contract_scope.name, &entry.name), + span: entry.span, + resolution: entry.resolution.clone(), + }, + ); + } + for entry in &contract_scope.terms { + if matches!( + entry.resolution, + Resolution::Def { + def, + kind: DefResolutionKind::Function, + } if private_functions.contains(&def) + ) { + continue; + } + self.terms.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::Silent, + ScopeEntry { + name: qualify(&contract_scope.name, &entry.name), + span: entry.span, + resolution: entry.resolution.clone(), + }, + ); + } + } + self.contracts.push(contract_scope); + } + + fn add_import_modules( + &mut self, + path: &[SpannedElem<'db, Ident<'db>>], + alias: Option>>, + ) { + if path.is_empty() { + return; + } + if let Some(alias) = alias { + self.add_module( + ident_text_str(self.db, &alias).to_owned(), + alias.span(self.db), + ); + return; + } + let full = path + .iter() + .map(|segment| ident_text_str(self.db, segment)) + .collect::>() + .join("."); + let leaf = path.last().expect("non-empty path"); + self.add_module(ident_text_str(self.db, leaf).to_owned(), leaf.span(self.db)); + if full != ident_text_str(self.db, leaf) { + self.add_module(full, path_span(self.db, path)); + } + } + + fn add_module(&mut self, name: String, span: Span<'db>) { + if self.modules.contains_name(&name) { + return; + } + self.modules.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::Silent, + ScopeEntry { + name: name.clone(), + span, + resolution: Resolution::Module(ModuleRef { + owner: self.module.def_id_value(self.db), + name, + }), + }, + ); + } +} + +fn type_decl_families_can_share(left: TypeDeclFamily, right: TypeDeclFamily) -> bool { + matches!( + (left, right), + (TypeDeclFamily::Adt, TypeDeclFamily::Contract) + | (TypeDeclFamily::Contract, TypeDeclFamily::Adt) + ) +} + +struct ContractScopeBuilder<'db> { + db: &'db dyn Db, + contract: DefId<'db>, + name: String, + context: String, + types: ScopeTableBuilder<'db>, + terms: ScopeTableBuilder<'db>, + fields: Vec>, + ctor_lists: Vec>, + external_functions: Vec>, + diagnostics: Vec, +} + +impl<'db> ContractScopeBuilder<'db> { + fn new(db: &'db dyn Db, contract: DefId<'db>, declaration_keyword: &str, name: String) -> Self { + let context = format!("{declaration_keyword} {name}"); + Self { + db, + contract, + name, + context, + types: ScopeTableBuilder::single_span(Namespace::Type), + terms: ScopeTableBuilder::single_span(Namespace::Term), + fields: Vec::new(), + ctor_lists: Vec::new(), + external_functions: Vec::new(), + diagnostics: Vec::new(), + } + } + + fn finish(self) -> (ContractScope<'db>, Vec) { + ( + ContractScope { + contract: self.contract, + name: self.name, + types: self.types.into_table(), + terms: self.terms.into_table(), + external_functions: self.external_functions, + fields: self.fields, + ctor_lists: self.ctor_lists, + }, + self.diagnostics, + ) + } + + fn add_type(&mut self, name: String, span: Span<'db>, resolution: Resolution<'db>) { + self.types.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::SingleSpan { + context: Some(&self.context), + }, + ScopeEntry { + name, + span, + resolution, + }, + ); + } + + fn add_term(&mut self, name: String, span: Span<'db>, resolution: Resolution<'db>) { + self.terms.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::SingleSpan { + context: Some(&self.context), + }, + ScopeEntry { + name, + span, + resolution, + }, + ); + } + + fn add_silent_term(&mut self, name: String, span: Span<'db>, resolution: Resolution<'db>) { + self.terms.push( + self.db, + &mut self.diagnostics, + DuplicatePolicy::Silent, + ScopeEntry { + name, + span, + resolution, + }, + ); + } + + fn add_field(&mut self, field: &FieldDef<'db>, index: u32) { + self.fields.push(FieldEntry { + name: ident_text_str(self.db, field.name()).to_owned(), + span: field.name().span(self.db), + field: FieldId { + contract: self.contract, + index: FieldIndex::from_u32(index), + }, + }); + } +} diff --git a/crates/hir/src/nameres/type_resolver.rs b/crates/hir/src/nameres/type_resolver.rs new file mode 100644 index 00000000..d0d12b3f --- /dev/null +++ b/crates/hir/src/nameres/type_resolver.rs @@ -0,0 +1,341 @@ +use super::*; + +pub(super) struct TypeResolver<'db, 'a> { + db: &'db dyn Db, + scope: &'a ItemScopeFacts<'db>, + imports: &'a dyn ImportedNames<'db>, + contract: Option>, + type_vars: Vec>, + seen_types: FxHashSet>, + seen_preds: FxHashSet>, + pub(super) map: ItemResolutionMap<'db>, +} + +impl<'db, 'a> TypeResolver<'db, 'a> { + pub(super) fn new( + db: &'db dyn Db, + scope: &'a ItemScopeFacts<'db>, + imports: &'a dyn ImportedNames<'db>, + ) -> Self { + Self { + db, + scope, + imports, + contract: None, + type_vars: Vec::new(), + seen_types: FxHashSet::default(), + seen_preds: FxHashSet::default(), + map: ItemResolutionMap::default(), + } + } + + pub(super) fn item( + &mut self, + item: Item<'db>, + contract: Option>, + inherited_type_vars: &[TypeVarBinding<'db>], + ) { + let old_contract = self.contract; + if let Some(contract) = contract { + self.contract = Some(contract.def_id_value(self.db)); + } + let old_len = self.type_vars.len(); + self.type_vars.extend_from_slice(inherited_type_vars); + match item { + Item::FunctionDef(def) => self.function(def), + Item::TypeAlias(def) => { + self.with_item_type_vars( + def.def_id_value(self.db), + def.ty_param_elems(self.db), + |this| { + this.ty(def.ty(this.db)); + }, + ); + } + Item::AdtDef(def) => { + self.with_item_type_vars( + def.def_id_value(self.db), + def.ty_param_elems(self.db), + |this| { + for ctor in def.ctors(this.db) { + this.ty(*ctor.fields.atom()); + } + }, + ); + } + Item::ClassDef(def) => { + self.with_item_type_vars( + def.def_id_value(self.db), + def.type_var_elems(self.db), + |this| { + for pred in def.super_preds(this.db) { + this.pred(*pred); + } + this.pred(def.head(this.db)); + for method in def.methods(this.db) { + let old_len = this.type_vars.len(); + this.type_vars.extend(type_var_bindings_from( + def.def_id_value(this.db), + def.type_var_elems(this.db).len() as u32, + &method.type_vars, + )); + this.sig(method); + this.type_vars.truncate(old_len); + } + }, + ); + } + Item::InstanceDef(def) => { + self.with_item_type_vars( + def.def_id_value(self.db), + def.type_var_elems(self.db), + |this| { + for pred in def.preds(this.db) { + this.pred(*pred); + } + this.pred(def.head(this.db)); + for method in def.methods(this.db) { + this.function(*method); + } + }, + ); + } + Item::ContractDef(def) => { + self.contract = Some(def.def_id_value(self.db)); + self.with_item_type_vars( + def.def_id_value(self.db), + def.ty_param_elems(self.db), + |this| { + for field in def.fields(this.db) { + this.ty(field.ty()); + } + for item in def.items(this.db) { + match *item { + ContractItem::FunctionDef(defn) => { + this.item(Item::FunctionDef(defn), Some(def), &[]) + } + ContractItem::TypeAlias(defn) => { + this.item(Item::TypeAlias(defn), Some(def), &[]) + } + ContractItem::AdtDef(defn) => { + this.item(Item::AdtDef(defn), Some(def), &[]) + } + ContractItem::Error { .. } => {} + } + } + }, + ); + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } + self.type_vars.truncate(old_len); + self.contract = old_contract; + } + + fn function(&mut self, def: FunctionDef<'db>) { + let sig = def.sig(self.db); + self.with_item_type_vars(def.def_id_value(self.db), &sig.type_vars, |this| { + this.sig(sig) + }); + } + + fn sig(&mut self, sig: &FuncSig<'db>) { + for pred in &sig.preds { + self.pred(*pred); + } + for param in sig.params.atom() { + self.param(param); + } + if let Some(ret) = sig.ret { + self.ty(ret); + } + } + + fn param(&mut self, param: &FuncParam<'db>) { + if let FuncParam::Typed { ty, .. } = param { + self.ty(*ty); + } + } + + fn pred(&mut self, pred: PredRef<'db>) { + if !self.seen_preds.insert(pred) { + return; + } + let kind = pred.kind(self.db); + self.ty(kind.ty); + for arg in kind.args.atom() { + self.ty(*arg); + } + let name = ident_text_str(self.db, &kind.class); + let resolution = self.lookup_class(name).unwrap_or_else(|| { + self.map + .diagnostics + .push(undefined_class(self.db, name, kind.class.span(self.db))); + Resolution::Err + }); + self.map.preds.push(PredResolution { pred, resolution }); + } + + fn ty(&mut self, ty: TypeRef<'db>) { + if !self.seen_types.insert(ty) { + return; + } + match ty.kind(self.db) { + TypeRefKind::Named { + qualifier, + name, + args, + } => { + for arg in args.atom() { + self.ty(*arg); + } + let resolution = if let Some(qualifier) = qualifier { + let qualifier_text = ident_text_str(self.db, qualifier); + let qualified = qualify(qualifier_text, ident_text_str(self.db, name)); + self.lookup_type(&qualified).unwrap_or_else(|| { + if self + .imports + .has_incomplete_module_qualifier(self.db, qualifier_text) + { + return Resolution::Err; + } + self.map + .diagnostics + .push(self.undefined_type_ctor_diag(&qualified, name.span(self.db))); + Resolution::Err + }) + } else { + let name_text = ident_text_str(self.db, name); + self.lookup_type(name_text).unwrap_or_else(|| { + self.map + .diagnostics + .push(self.undefined_type_ctor_diag(name_text, name.span(self.db))); + Resolution::Err + }) + }; + self.map.types.push(TypeResolution { ty, resolution }); + } + TypeRefKind::FixedArray { element, .. } => self.ty(*element), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + self.ty(*param); + } + self.ty(*ret); + } + TypeRefKind::Comptime { inner, .. } => self.ty(*inner), + TypeRefKind::Tuple { elems } => { + for elem in elems.atom() { + self.ty(*elem); + } + } + TypeRefKind::Error { .. } => { + self.map.types.push(TypeResolution { + ty, + resolution: Resolution::Err, + }); + } + } + } + + fn with_item_type_vars( + &mut self, + owner: DefId<'db>, + vars: &[SpannedElem<'db, Ident<'db>>], + f: impl FnOnce(&mut Self), + ) { + let old_len = self.type_vars.len(); + self.type_vars.extend(type_var_bindings(owner, vars)); + f(self); + self.type_vars.truncate(old_len); + } + + fn lookup_type(&self, name: &str) -> Option> { + self.type_vars + .iter() + .rev() + .find(|var| ident_text_str(self.db, &var.name) == name) + .map(|var| { + Resolution::Local(LocalBinding::TypeVar(TypeVarId { + owner: var.owner, + index: var.index, + name: name.to_owned(), + })) + }) + .or_else(|| { + self.contract + .and_then(|contract| self.scope.contract_scope(contract)) + .and_then(|contract| contract.type_resolution(name)) + }) + .or_else(|| self.scope.type_resolution(name)) + .or_else(|| self.imports.imported(self.db, Namespace::Type, name)) + .or_else(|| builtin_type_or_class(name)) + .or_else(|| { + self.imports + .may_contain_unknown_unqualified(self.db, Namespace::Type, name) + .then_some(Resolution::Err) + }) + } + + fn lookup_class(&self, name: &str) -> Option> { + match self.lookup_type(name) { + Some( + res @ Resolution::Def { + kind: DefResolutionKind::Class, + .. + }, + ) + | Some(res @ Resolution::Builtin(BuiltinKind::Class(_))) + | Some(res @ Resolution::Err) => Some(res), + Some(_) | None => None, + } + } + + fn undefined_type_ctor_diag(&self, name: &str, span: Span<'db>) -> NameresDiagnostic { + let constructor_candidate = unique_constructor_type_candidate( + self.constructor_type_candidates(name) + .into_iter() + .filter(|candidate| candidate.ctor_name == name), + ); + let suggestion = constructor_candidate + .is_none() + .then(|| best_name_suggestion(name, self.type_candidate_names())) + .flatten(); + undefined_type_ctor(self.db, name, span, suggestion, constructor_candidate) + } + + fn type_candidate_names(&self) -> Vec { + let mut names = Vec::new(); + names.extend( + self.type_vars + .iter() + .map(|var| ident_text_str(self.db, &var.name).to_owned()), + ); + if let Some(contract) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + { + names.extend(contract.types.iter().map(|entry| entry.name.clone())); + } + names.extend(self.scope.types.iter().map(|entry| entry.name.clone())); + names.extend(self.imports.candidate_names(self.db, Namespace::Type)); + names + } + + fn constructor_type_candidates(&self, leaf: &str) -> Vec { + let mut candidates = Vec::new(); + if let Some(contract) = self + .contract + .and_then(|contract| self.scope.contract_scope(contract)) + { + collect_constructor_type_candidates( + self.db, + &contract.ctor_lists, + leaf, + &mut candidates, + ); + } + collect_constructor_type_candidates(self.db, &self.scope.ctor_lists, leaf, &mut candidates); + candidates.extend(self.imports.constructor_type_candidates(self.db, leaf)); + candidates + } +} diff --git a/crates/hir/src/nameres/util.rs b/crates/hir/src/nameres/util.rs new file mode 100644 index 00000000..dfe3ba67 --- /dev/null +++ b/crates/hir/src/nameres/util.rs @@ -0,0 +1,167 @@ +use super::*; + +pub(super) fn record_module_fields<'db>(db: &'db dyn Db, module: Module<'db>) { + if tracing::enabled!(target: "hir::query", Level::DEBUG) { + record_def_fields(db, module.def_id_value(db)); + } +} + +pub(super) fn record_body_fields<'db>(db: &'db dyn Db, body: FuncBody<'db>) { + if tracing::enabled!(target: "hir::query", Level::DEBUG) { + record_def_fields(db, body.def_id(db)); + } +} + +fn record_def_fields<'db>(db: &'db dyn Db, def: DefId<'db>) { + let span = tracing::Span::current(); + span.record("file", field::display(file_url_tail(db, def.file(db)))); + span.record("def", field::display(def_name(db, def))); +} + +fn def_name<'db>(db: &'db dyn Db, def: DefId<'db>) -> String { + def.name(db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("{:?}", def.kind(db))) +} + +fn file_url_tail(db: &dyn Db, file: crate::input::SourceFile) -> String { + let url = file.url(db); + if let Some(mut segments) = url.path_segments() + && let Some(last) = segments.next_back() + && !last.is_empty() + { + return last.to_owned(); + } + url.as_str() + .rsplit('/') + .next() + .filter(|tail| !tail.is_empty()) + .unwrap_or(url.as_str()) + .to_owned() +} + +pub fn ident_text<'db>(db: &'db dyn Db, ident: &SpannedElem<'db, Ident<'db>>) -> String { + ident_text_str(db, ident).to_owned() +} + +pub(super) fn ident_text_str<'db>( + db: &'db dyn Db, + ident: &SpannedElem<'db, Ident<'db>>, +) -> &'db str { + (*ident.atom()).text(db) +} + +pub(super) fn collect_constructor_type_candidates<'db>( + db: &'db dyn Db, + lists: &[CtorList<'db>], + leaf: &str, + out: &mut Vec, +) { + for list in lists { + for ctor in &list.ctors { + if ctor.name == leaf { + out.push(ConstructorTypeCandidate { + ty_name: list.ty_name.clone(), + ctor_name: ctor.name.clone(), + span: LabelSpan::from_span(db, ctor.span), + }); + } + } + } +} + +pub(super) fn unique_constructor_type_candidate( + candidates: impl IntoIterator, +) -> Option { + let mut candidates = candidates.into_iter(); + let first = candidates.next()?; + if candidates.next().is_some() { + return None; + } + Some(first) +} + +pub(super) fn qualify(qualifier: &str, name: &str) -> String { + format!("{qualifier}.{name}") +} + +pub(super) fn path_span<'db>(db: &'db dyn Db, path: &[SpannedElem<'db, Ident<'db>>]) -> Span<'db> { + let first = path.first().expect("non-empty path"); + let last = path.last().expect("non-empty path"); + first.span(db) + last.span(db) +} + +pub(super) fn expr_path<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + expr: Id>, +) -> Option> { + match &body.exprs(db).get(expr).kind { + ExprKind::Ident(name) => Some(vec![ident_text_str(db, name).to_owned()]), + ExprKind::Field { base, field } => { + let mut path = expr_path(db, body, *base)?; + path.push(ident_text_str(db, field).to_owned()); + Some(path) + } + _ => None, + } +} + +pub(super) fn param_name<'a, 'db>( + param: &'a FuncParam<'db>, +) -> Option<&'a SpannedElem<'db, Ident<'db>>> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => Some(name), + FuncParam::Error { .. } => None, + } +} + +pub fn param_bindings<'db>(params: &[FuncParam<'db>]) -> Vec> { + params + .iter() + .filter_map(param_name) + .map(|name| ParamBinding { name: *name }) + .collect() +} + +pub fn type_var_bindings<'db>( + owner: DefId<'db>, + vars: &[SpannedElem<'db, Ident<'db>>], +) -> Vec> { + type_var_bindings_from(owner, 0, vars) +} + +/// Builds type-variable bindings whose owner-local indexes start at `offset`. +/// +/// Class method `forall` binders share the class definition as their stable +/// owner, so their indexes follow the enclosing class binders. +pub fn type_var_bindings_from<'db>( + owner: DefId<'db>, + offset: u32, + vars: &[SpannedElem<'db, Ident<'db>>], +) -> Vec> { + vars.iter() + .enumerate() + .map(|(index, name)| TypeVarBinding { + owner, + name: *name, + index: offset + index as u32, + }) + .collect() +} + +pub fn is_direct_call_resolution(resolution: &Resolution<'_>) -> bool { + matches!( + resolution, + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } | Resolution::Ctor { .. } + | Resolution::ClassMethod { .. } + | Resolution::Builtin( + BuiltinKind::Constructor(_) + | BuiltinKind::Function(_) + | BuiltinKind::ClassMethod(_) + ) + ) +} diff --git a/crates/hir/src/sema.rs b/crates/hir/src/sema.rs index 87caf605..59f15cb5 100644 --- a/crates/hir/src/sema.rs +++ b/crates/hir/src/sema.rs @@ -1 +1,5 @@ +//! Semantic representation produced after HIR name resolution and type +//! analysis. + +/// Checked type, predicate, and scheme values. pub mod ty; diff --git a/crates/hir/src/sema/ty.rs b/crates/hir/src/sema/ty.rs index 949ee33a..60203924 100644 --- a/crates/hir/src/sema/ty.rs +++ b/crates/hir/src/sema/ty.rs @@ -1,229 +1,442 @@ -use crate::{ - Db, - ast::{ - Ident, - item::{AdtDef, ClassDef, ContractDef, TypeAlias}, - }, -}; - +//! Ground semantic types and predicates. +//! +//! This module is separate from `ast::ty`: AST type references preserve source +//! syntax before name resolution, while `Ty`, `Pred`, and `TyScheme` represent +//! normalized semantic objects that later type checking and inference work +//! with. Values are interned through Salsa so structurally equal ground types +//! can be compared and shared cheaply. +//! +//! Inference variables are intentionally absent from these interned values. +//! Type inference uses ephemeral `InferTy` values in `solcore-hir-ty` and +//! converts them back to `Ty` only at query boundaries. + +use std::fmt; + +use crate::{Db, anchor::DefId}; + +/// Interned semantic type. +/// +/// A `Ty` is a ground semantic shape: names have been resolved to builtins, +/// user constructors, or de Bruijn-bound variables. `TyKind::Unknown` lets +/// inference publish a placeholder when an ephemeral variable cannot yet be +/// made ground; it is not itself an inference variable and carries no solver +/// identity. #[salsa::interned(debug)] pub struct Ty<'db> { + /// Semantic type payload. #[returns(ref)] pub kind: TyKind<'db>, } +/// Shape of a ground semantic type. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum TyKind<'db> { + /// Error sentinel used after an earlier diagnostic. Error, - - Var(TyVar<'db>), - - /// Inference meta variable (unification variable). - Meta(InferenceVar), - + /// Unknown placeholder used at inference query boundaries. + Unknown, + /// De Bruijn-bound type variable. + BoundVar(BoundTyVar), + /// Type constructor application. Named { + /// Resolved constructor. ctor: TyCtor<'db>, + /// Type arguments. args: Vec>, }, - + /// Function type. Function { + /// Parameter types. params: Vec>, + /// Return type. ret: Ty<'db>, }, - + /// Tuple type, including unit when the vector is empty. Tuple(Vec>), + /// `comptime` type wrapper. + Comptime(Ty<'db>), } -/// Inference-only unification variable identifier. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub struct InferenceVar(u32); - +/// De Bruijn index for a type variable bound by an enclosing scheme. +/// +/// Index `0` names the first binder in the scheme's binder list. The index is +/// scoped by the scheme that owns the type and is deliberately independent of +/// the HIR definition that introduced the binder. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub enum TyVarFlavor { - Bound, - Skolem, -} - -#[salsa::interned(debug)] -pub struct TyVar<'db> { - #[returns(copy)] - pub name: Ident<'db>, - pub flavor: TyVarFlavor, +pub struct BoundTyVar { + /// Zero-based binder index in the owning scheme. + pub index: u32, } +/// Resolved type constructor. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum TyCtor<'db> { + /// Compiler-defined constructor. Builtin(BuiltinTyCtor), + /// User-defined constructor. User(UserTyCtor<'db>), } +/// Built-in type constructors. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum BuiltinTyCtor { + /// Machine word type. Word, + /// Unit type. Unit, + /// Boolean type. Bool, + /// String type. String, + /// Comptime-only arbitrary-precision integer type. + Integer, + /// Binary product constructor. Pair, + /// Binary sum constructor. Sum, + /// Fixed-length array constructor carrying its element count. + FixedArray(u64), +} + +/// User-defined type constructor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub struct UserTyCtor<'db> { + /// Definition identity of the constructor. + pub def: DefId<'db>, + /// Kind of user type constructor. + pub kind: UserTyCtorKind, } +/// Kind of user-defined type constructor. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] -pub enum UserTyCtor<'db> { - Adt(AdtDef<'db>), - Alias(TypeAlias<'db>), - Contract(ContractDef<'db>), +pub enum UserTyCtorKind { + /// Algebraic data type constructor. + Adt, + /// Type alias constructor. + Alias, + /// Nominal user-defined value type constructor. + ValueType, + /// Contract type constructor. + Contract, } +/// Resolved type-class identifier. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum ClassId<'db> { + /// Compiler-defined class. + Builtin(BuiltinClassId), + /// User-defined class. + User(DefId<'db>), +} + +/// Built-in class identifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] +pub enum BuiltinClassId { + /// `invokable`. + Invokable, + /// Reserved integer-literal class `Int`. + Int, +} + +/// Interned semantic predicate. #[salsa::interned(debug)] pub struct Pred<'db> { + /// Predicate payload. #[returns(ref)] pub kind: PredKind<'db>, } +/// Shape of a semantic predicate. #[derive(Debug, Clone, PartialEq, Eq, Hash, salsa::Update)] pub enum PredKind<'db> { + /// Type-class membership predicate. InClass { - class: ClassDef<'db>, + /// Resolved class identifier. + class: ClassId<'db>, + /// Main constrained type. main: Ty<'db>, + /// Additional class arguments. args: Vec>, }, - + /// Type equality predicate. Eq { + /// Left-hand type. lhs: Ty<'db>, + /// Right-hand type. rhs: Ty<'db>, }, - + /// Error sentinel used after an earlier diagnostic. Error, } +/// Type qualified by a list of predicates. #[salsa::interned(debug)] pub struct QualTy<'db> { + /// Required predicates. #[returns(ref)] pub preds: Vec>, + /// Underlying type. pub ty: Ty<'db>, } +/// Polymorphic type scheme. +/// +/// Schemes quantify a fixed number of de Bruijn binders around a qualified +/// body type. Monomorphic types have `binder_count == 0`. #[salsa::interned(debug)] pub struct TyScheme<'db> { - #[returns(ref)] - pub vars: Vec>, + /// Number of binders in scope for `body`. + #[returns(copy)] + pub binder_count: u32, + /// Qualified body type. pub body: QualTy<'db>, } +impl BoundTyVar { + /// Creates a bound type-variable reference. + pub const fn new(index: u32) -> Self { + Self { index } + } +} + impl BuiltinTyCtor { + /// Returns the number of type arguments required by this builtin + /// constructor. pub const fn arity(self) -> usize { match self { - Self::Word | Self::Unit | Self::Bool | Self::String => 0, + Self::Word | Self::Unit | Self::Bool | Self::String | Self::Integer => 0, + Self::FixedArray(_) => 1, Self::Pair | Self::Sum => 2, } } + /// Looks up a builtin constructor by source name. + /// + /// Returns `None` for user-defined names or non-type builtins. pub fn by_name(name: &str) -> Option { match name { "word" => Some(Self::Word), "()" => Some(Self::Unit), "bool" => Some(Self::Bool), "string" => Some(Self::String), + "integer" => Some(Self::Integer), "pair" => Some(Self::Pair), "sum" => Some(Self::Sum), _ => None, } } -} - -impl<'db> TyVar<'db> { - pub fn bound(db: &'db dyn Db, name: Ident<'db>) -> Self { - Self::new(db, name, TyVarFlavor::Bound) - } - pub fn skolem(db: &'db dyn Db, name: Ident<'db>) -> Self { - Self::new(db, name, TyVarFlavor::Skolem) + /// Returns the canonical source spelling for this builtin constructor. + pub const fn name(self) -> &'static str { + match self { + Self::Word => "word", + Self::Unit => "()", + Self::Bool => "bool", + Self::String => "string", + Self::Integer => "integer", + Self::Pair => "pair", + Self::Sum => "sum", + Self::FixedArray(_) => "fixed_array", + } } +} - pub fn is_bound(self, db: &'db dyn Db) -> bool { - matches!(self.flavor(db), TyVarFlavor::Bound) +impl BuiltinClassId { + /// Returns the canonical source spelling for this builtin class. + pub const fn name(self) -> &'static str { + match self { + Self::Invokable => "invokable", + Self::Int => "Int", + } } } impl<'db> Ty<'db> { + /// Creates an error type sentinel. pub fn error(db: &'db dyn Db) -> Self { Self::new(db, TyKind::Error) } - pub fn var(db: &'db dyn Db, var: TyVar<'db>) -> Self { - Self::new(db, TyKind::Var(var)) + /// Creates an unknown type placeholder. + pub fn unknown(db: &'db dyn Db) -> Self { + Self::new(db, TyKind::Unknown) } - pub fn meta(db: &'db dyn Db, var: InferenceVar) -> Self { - Self::new(db, TyKind::Meta(var)) + /// Creates a de Bruijn-bound type-variable reference. + pub fn bound(db: &'db dyn Db, index: u32) -> Self { + Self::new(db, TyKind::BoundVar(BoundTyVar::new(index))) } + /// Creates a constructor application. + /// + /// The function does not validate arity; callers that resolve constructors + /// are responsible for checking argument counts. pub fn named(db: &'db dyn Db, ctor: TyCtor<'db>, args: Vec>) -> Self { Self::new(db, TyKind::Named { ctor, args }) } + /// Creates a function type. pub fn function(db: &'db dyn Db, params: Vec>, ret: Ty<'db>) -> Self { Self::new(db, TyKind::Function { params, ret }) } + /// Creates a tuple type. pub fn tuple(db: &'db dyn Db, elems: Vec>) -> Self { Self::new(db, TyKind::Tuple(elems)) } + /// Creates a `comptime` type wrapper. + pub fn comptime(db: &'db dyn Db, inner: Ty<'db>) -> Self { + Self::new(db, TyKind::Comptime(inner)) + } + + /// Alias for [`Ty::function`] kept for callers that use type-theory naming. pub fn funtype(db: &'db dyn Db, params: Vec>, ret: Ty<'db>) -> Self { Self::function(db, params, ret) } + /// Creates a nullary builtin type constructor application. + /// + /// For non-nullary builtins such as `pair` and `sum`, callers should use + /// [`Ty::named`] with explicit arguments instead. pub fn builtin(db: &'db dyn Db, ctor: BuiltinTyCtor) -> Self { Self::named(db, TyCtor::Builtin(ctor), Vec::new()) } + /// Creates the builtin `word` type. pub fn word(db: &'db dyn Db) -> Self { Self::builtin(db, BuiltinTyCtor::Word) } + /// Creates the builtin unit type. pub fn unit(db: &'db dyn Db) -> Self { Self::builtin(db, BuiltinTyCtor::Unit) } + /// Creates the builtin `bool` type. pub fn bool(db: &'db dyn Db) -> Self { Self::builtin(db, BuiltinTyCtor::Bool) } + /// Creates the builtin `string` type. pub fn string(db: &'db dyn Db) -> Self { Self::builtin(db, BuiltinTyCtor::String) } + /// Creates the builtin comptime-only `integer` type. + pub fn integer(db: &'db dyn Db) -> Self { + Self::builtin(db, BuiltinTyCtor::Integer) + } + + /// Creates a fixed-length array type. + pub fn fixed_array(db: &'db dyn Db, element: Ty<'db>, length: u64) -> Self { + Self::named( + db, + TyCtor::Builtin(BuiltinTyCtor::FixedArray(length)), + vec![element], + ) + } + + /// Returns a structural size measure for termination checks. pub fn measure(self, db: &'db dyn Db) -> usize { match self.kind(db) { - TyKind::Error | TyKind::Var(_) | TyKind::Meta(_) => 1, + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => 1, TyKind::Named { args, .. } => 1 + args.iter().map(|it| it.measure(db)).sum::(), TyKind::Function { params, ret } => { 1 + params.iter().map(|it| it.measure(db)).sum::() + ret.measure(db) } TyKind::Tuple(elems) => 1 + elems.iter().map(|it| it.measure(db)).sum::(), + TyKind::Comptime(inner) => 1 + inner.measure(db), + } + } + + /// Returns a stable human-readable type snapshot for diagnostics. + pub fn display(self, db: &'db dyn Db) -> String { + match self.kind(db) { + TyKind::Error => "".to_owned(), + TyKind::Unknown | TyKind::BoundVar(_) => "_".to_owned(), + TyKind::Named { ctor, args } => { + if let TyCtor::Builtin(BuiltinTyCtor::FixedArray(length)) = ctor + && let [element] = args.as_slice() + { + return format!("{}[{length}]", element.display(db)); + } + let name = match ctor { + TyCtor::Builtin(ctor) => ctor.name().to_owned(), + TyCtor::User(user) => { + let def = user + .def + .name(db) + .unwrap_or_else(|| format!("{:?}", user.def.kind(db))); + format!("{}:{def}", user.kind) + } + }; + if args.is_empty() { + name + } else { + format!( + "{name}<{}>", + args.iter() + .map(|arg| arg.display(db)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Function { params, ret } => { + let params = params + .iter() + .map(|param| param.display(db)) + .collect::>() + .join(", "); + let ret = ret.display(db); + if ret == "()" { + format!("function({params})") + } else { + format!("function({params}) returns ({ret})") + } + } + TyKind::Tuple(elems) => { + if elems.is_empty() { + "()".to_owned() + } else { + format!( + "({})", + elems + .iter() + .map(|elem| elem.display(db)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Comptime(inner) => format!("comptime {}", inner.display(db)), } } } impl<'db> Pred<'db> { + /// Creates a type-class membership predicate. pub fn in_class( db: &'db dyn Db, - class: ClassDef<'db>, + class: ClassId<'db>, main: Ty<'db>, args: Vec>, ) -> Self { Self::new(db, PredKind::InClass { class, main, args }) } + /// Creates a type equality predicate. pub fn eq(db: &'db dyn Db, lhs: Ty<'db>, rhs: Ty<'db>) -> Self { Self::new(db, PredKind::Eq { lhs, rhs }) } + /// Creates an error predicate sentinel. pub fn error(db: &'db dyn Db) -> Self { Self::new(db, PredKind::Error) } + /// Returns a structural size measure for termination checks. pub fn measure(self, db: &'db dyn Db) -> usize { match self.kind(db) { PredKind::InClass { main, args, .. } => { @@ -233,16 +446,85 @@ impl<'db> Pred<'db> { PredKind::Error => 1, } } + + /// Returns a stable human-readable predicate snapshot for diagnostics. + pub fn display(self, db: &'db dyn Db) -> String { + match self.kind(db) { + PredKind::InClass { class, main, args } => { + let class = match class { + ClassId::Builtin(class) => class.name().to_owned(), + ClassId::User(def) => { + format!( + "trait:{}", + def.name(db) + .unwrap_or_else(|| format!("{:?}", def.kind(db))) + ) + } + }; + if args.is_empty() { + format!("{}: {class}", main.display(db)) + } else { + format!( + "{}: {class}<{}>", + main.display(db), + args.iter() + .map(|arg| arg.display(db)) + .collect::>() + .join(", ") + ) + } + } + PredKind::Eq { lhs, rhs } => format!("{} ~ {}", lhs.display(db), rhs.display(db)), + PredKind::Error => "".to_owned(), + } + } } impl<'db> QualTy<'db> { + /// Creates a qualified type with no predicates. pub fn monotype(db: &'db dyn Db, ty: Ty<'db>) -> Self { Self::new(db, Vec::new(), ty) } } impl<'db> TyScheme<'db> { + /// Creates a monomorphic scheme from a type. pub fn monotype(db: &'db dyn Db, ty: Ty<'db>) -> Self { - Self::new(db, Vec::new(), QualTy::monotype(db, ty)) + Self::new(db, 0, QualTy::monotype(db, ty)) + } + + /// Returns a stable human-readable scheme snapshot for diagnostics. + pub fn display(self, db: &'db dyn Db) -> String { + let body = self.body(db); + let preds = body + .preds(db) + .iter() + .map(|pred| pred.display(db)) + .collect::>(); + let qualified = if preds.is_empty() { + body.ty(db).display(db) + } else { + format!("{} where {}", body.ty(db).display(db), preds.join(", ")) + }; + if self.binder_count(db) == 0 { + qualified + } else { + let vars = (0..self.binder_count(db)) + .map(|_| "_".to_owned()) + .collect::>() + .join(", "); + format!("<{vars}> {qualified}") + } + } +} + +impl fmt::Display for UserTyCtorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Adt => f.write_str("adt"), + Self::Alias => f.write_str("alias"), + Self::ValueType => f.write_str("value type"), + Self::Contract => f.write_str("contract"), + } } } diff --git a/crates/hir/src/span.rs b/crates/hir/src/span.rs index 98c421a6..acddaec9 100644 --- a/crates/hir/src/span.rs +++ b/crates/hir/src/span.rs @@ -1,62 +1,117 @@ +//! Anchor-relative source spans. +//! +//! HIR spans are stored as byte offsets relative to an +//! [`crate::span::AnchorId`] instead of as absolute file offsets. Root anchors +//! are file-relative; definition anchors are relative to the current base +//! offset of a stable [`crate::anchor::DefId`]. That design lets semantic Salsa +//! queries stay byte-shift invariant: moving a function down in a file changes +//! the def-location table, but not every span inside the function body. +//! +//! Absolute resolution is therefore an edge-only operation. Diagnostics, LSP, +//! CLI output, and other presentation boundaries may call +//! [`crate::span::Span::resolve_to_absolute`], +//! [`crate::span::AnchorId::source_file`], or +//! [`crate::span::AnchorId::base_offset`]. Tracked semantic queries should keep +//! spans relative, because reading the location table would backdate otherwise +//! stable results and cause broad re-execution after unrelated edits. + use std::ops::Add; use crate::{ Db, - anchor::{DefId, def_locations_for_file, resolve_def_location}, + anchor::{DefId, resolve_def_location_or_bug}, diag::{AbsoluteSpan, Offset}, input::SourceFile, }; +/// The base object that gives meaning to a relative span. +/// +/// `Root` anchors make offsets relative to the beginning of a source file. +/// `Def` anchors make offsets relative to the recorded base offset of a +/// definition. A def anchor is only resolvable while the database can provide a +/// matching location entry for that definition. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub enum AnchorKind<'db> { + /// Offsets are absolute within this source file. Root(SourceFile), + /// Offsets are relative to this definition's current base location. Def(DefId<'db>), } +/// Interned handle for a span anchor. +/// +/// Interning keeps anchor values cheap to copy through HIR nodes. The anchor is +/// a semantic identity, not a resolved file position; resolving def anchors is +/// intentionally deferred to output edges. #[salsa::interned(debug)] pub struct AnchorId<'db> { + /// The root file or definition used as this anchor's base. #[returns(ref)] kind: AnchorKind<'db>, } impl<'db> AnchorId<'db> { + /// Creates the root anchor for `file`. + /// + /// Spans using this anchor store offsets from byte `0` of the file and can + /// resolve without consulting the def-location table. pub fn root(db: &'db dyn Db, file: SourceFile) -> Self { Self::new(db, AnchorKind::Root(file)) } + /// Creates an anchor relative to `def`. + /// + /// The anchor is valid for semantic storage immediately, but absolute + /// resolution later requires `Db::def_location_table(def.file(db))` to + /// contain a matching entry. pub fn def(db: &'db dyn Db, def: DefId<'db>) -> Self { Self::new(db, AnchorKind::Def(def)) } + /// Returns the anchor kind by value. + /// + /// This is cheap because both variants are copyable. For def anchors the + /// returned value still does not resolve the def to an absolute position. pub fn kind_value(self, db: &'db dyn Db) -> AnchorKind<'db> { *self.kind(db) } + /// Resolves the source file for this anchor. + /// + /// Edge-only: do not call this inside tracked semantic queries. Def anchors + /// read `def_location_table`, which changes on nearly any edit and would + /// over-invalidate otherwise byte-shift-invariant results. pub fn source_file(self, db: &'db dyn Db) -> SourceFile { match *self.kind(db) { AnchorKind::Root(file) => file, AnchorKind::Def(def) => { - let locations = def_locations_for_file(db, def.file(db)); - resolve_def_location(locations, def) - .unwrap_or_else(|| panic!("missing DefLocation for def anchor: {:?}", def)) - .file + let locations = db.def_location_table(def.file(db)); + resolve_def_location_or_bug(locations, def, "def anchor", def).file } } } + /// Resolves the absolute byte offset for this anchor's base. + /// + /// Edge-only: do not call this inside tracked semantic queries. Def anchors + /// read `def_location_table`, which changes on nearly any edit and would + /// over-invalidate otherwise byte-shift-invariant results. pub fn base_offset(self, db: &'db dyn Db) -> Offset { match *self.kind(db) { AnchorKind::Root(_) => Offset::new(0), AnchorKind::Def(def) => { - let locations = def_locations_for_file(db, def.file(db)); - resolve_def_location(locations, def) - .unwrap_or_else(|| panic!("missing DefLocation for def anchor: {:?}", def)) - .base_offset + let locations = db.def_location_table(def.file(db)); + resolve_def_location_or_bug(locations, def, "def anchor", def).base_offset } } } } +/// A half-open byte range relative to an anchor. +/// +/// `begin` and `end` are measured from the anchor's base, not necessarily from +/// the start of the source file. The invariant is `begin <= end`; empty spans +/// are allowed and commonly represent recovered or synthetic syntax positions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct Span<'db> { anchor: AnchorId<'db>, @@ -65,48 +120,81 @@ pub struct Span<'db> { } impl<'db> Span<'db> { + /// Creates a new anchor-relative half-open span. + /// + /// # Panics + /// + /// Panics when `begin > end`, because every consumer assumes monotonic byte + /// offsets. pub fn new(anchor: AnchorId<'db>, begin: Offset, end: Offset) -> Self { assert!(begin <= end, "span start must be <= end"); Self { anchor, begin, end } } + /// Returns the anchor that defines the coordinate system for this span. + /// + /// The result is a stable HIR handle. Callers that need file offsets must + /// resolve the span at an output edge instead of inside tracked queries. pub fn anchor(self) -> AnchorId<'db> { self.anchor } + /// Returns the starting byte offset relative to this span's anchor. + /// + /// For root anchors this is also the file offset; for def anchors it is + /// only meaningful after adding the def's current base offset. pub fn begin(self) -> Offset { self.begin } + /// Returns the exclusive ending byte offset relative to this span's anchor. + /// + /// The offset may equal [`Span::begin`] for zero-width spans produced by + /// recovery. pub fn end(self) -> Offset { self.end } + /// Resolves the source file for this span's anchor. + /// + /// This follows the same edge-only rule as [`AnchorId::source_file`]. It + /// may consult the def-location table for def anchors and panic if the + /// table is missing the definition. pub fn source_file(self, db: &'db dyn Db) -> SourceFile { self.anchor.source_file(db) } + /// Resolves this anchor-relative span to absolute file offsets. + /// + /// Edge-only: use this at diagnostics/LSP boundaries, not inside tracked + /// semantic queries. Def anchors depend on `def_location_table`, which + /// shifts on nearly any edit and would over-invalidate semantic results. pub fn resolve_to_absolute(self, db: &'db dyn Db) -> AbsoluteSpan { let file = self.anchor.source_file(db); let base = self.anchor.base_offset(db); - let start = add_offset(base, self.begin); - let end = add_offset(base, self.end); + let start = Offset::checked_add_or_bug(base, self.begin, "resolving span"); + let end = Offset::checked_add_or_bug(base, self.end, "resolving span"); AbsoluteSpan::new(file, start, end) } } -fn add_offset(base: Offset, rel: Offset) -> Offset { - let Some(raw) = base.as_u32().checked_add(rel.as_u32()) else { - panic!("offset overflow while resolving span"); - }; - Offset::new(raw) -} - impl<'db> Add for Span<'db> { type Output = Self; + /// Returns the smallest span covering both operands when they share an + /// anchor. + /// + /// Spans with different anchors cannot be combined without absolute + /// resolution, so release builds preserve the left operand after a debug + /// assertion. This keeps error-recovery code from manufacturing a span in + /// the wrong coordinate system. fn add(self, rhs: Self) -> Self { debug_assert_eq!(self.anchor, rhs.anchor); + if self.anchor != rhs.anchor { + // Spans with different anchors use incompatible bases; keep the + // left operand instead of mixing unrelated relative offsets. + return self; + } let begin = std::cmp::min(self.begin, rhs.begin); let end = std::cmp::max(self.end, rhs.end); Self { @@ -117,6 +205,11 @@ impl<'db> Add for Span<'db> { } } +/// A value paired with the source span that produced it. +/// +/// The wrapper is used throughout the HIR for names, parameter lists, and other +/// non-interned atoms where consumers need to report diagnostics against the +/// original syntax without making the atom itself span-aware. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)] pub struct SpannedElem<'db, T: salsa::Update> { atom: T, @@ -124,10 +217,12 @@ pub struct SpannedElem<'db, T: salsa::Update> { } impl<'db, T: salsa::Update> SpannedElem<'db, T> { + /// Pairs `atom` with its anchor-relative source span. pub fn new(atom: T, span: Span<'db>) -> Self { Self { atom, span } } + /// Returns the wrapped value without discarding its span. pub fn atom(&self) -> &T { &self.atom } @@ -139,6 +234,15 @@ impl<'db, T: salsa::Update> Spanned<'db> for SpannedElem<'db, T> { } } +/// Common interface for HIR nodes that can identify their source range. +/// +/// Implementations return anchor-relative spans. Callers must only resolve the +/// span to absolute offsets when they are producing diagnostics, editor data, +/// or other non-cached presentation artifacts. pub trait Spanned<'db> { + /// Returns the anchor-relative span covering this node's original syntax. + /// + /// Implementations may read interned/tracked HIR fields through `db`, but + /// should not force absolute span resolution. fn span(&self, db: &'db dyn Db) -> Span<'db>; } diff --git a/crates/hir/src/visit.rs b/crates/hir/src/visit.rs new file mode 100644 index 00000000..180801f2 --- /dev/null +++ b/crates/hir/src/visit.rs @@ -0,0 +1,311 @@ +//! HIR inspection helpers. +//! +//! This module currently exposes an error-node collector used by tests and +//! callers that need to distinguish parser recovery from later semantic errors. +//! It follows a silent-`Error` contract: recovered HIR nodes are collected as +//! data, not reported as diagnostics here. The parser/lowerer is responsible +//! for emitting parse diagnostics exactly once. + +use rustc_hash::FxHashSet; + +use crate::{ + Db, + ast::{ + function::{ + BinOp, Expr, ExprKind, FuncBody, FuncParam, FuncSig, LitKind, Pat, PatKind, Stmt, + StmtKind, UnOp, YulCase, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind, + }, + item::{ContractItem, FunctionDef, Item, Module}, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + span::{Span, Spanned}, +}; + +/// Recovered error placeholder found in lowered HIR. +/// +/// The `kind` names the enum variant that carried the placeholder, and `span` +/// is the anchor-relative source range of the recovered syntax. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ErrorNode<'db> { + /// Static enum-variant name for the recovered node. + pub kind: &'static str, + /// Anchor-relative range associated with the recovery node. + pub span: Span<'db>, +} + +/// Collects recovered `Error` nodes from a module without emitting diagnostics. +/// +/// This is intentionally a read-only inspection pass. It recurses through item +/// signatures, function bodies, nested lambda bodies, type references, and Yul +/// blocks, but it does not interpret names or types. +pub fn collect_error_nodes<'db>(db: &'db dyn Db, module: Module<'db>) -> Vec> { + let mut collector = ErrorCollector { + db, + errors: Vec::new(), + seen_types: FxHashSet::default(), + }; + for item in module.items(db) { + collector.item(*item); + } + collector.errors +} + +struct ErrorCollector<'db> { + db: &'db dyn Db, + errors: Vec>, + seen_types: FxHashSet>, +} + +impl<'db> ErrorCollector<'db> { + fn push(&mut self, kind: &'static str, span: Span<'db>) { + self.errors.push(ErrorNode { kind, span }); + } + + fn item(&mut self, item: Item<'db>) { + match item { + Item::FunctionDef(def) => self.function(def), + Item::TypeAlias(def) => self.ty(def.ty(self.db)), + Item::AdtDef(def) => { + for ctor in def.ctors(self.db) { + self.ty(*ctor.fields.atom()); + } + } + Item::ClassDef(def) => { + for pred in def.super_preds(self.db) { + self.pred(*pred); + } + self.pred(def.head(self.db)); + for method in def.methods(self.db) { + self.sig(method); + } + } + Item::InstanceDef(def) => { + for pred in def.preds(self.db) { + self.pred(*pred); + } + self.pred(def.head(self.db)); + for method in def.methods(self.db) { + self.function(*method); + } + } + Item::ContractDef(def) => { + for field in def.fields(self.db) { + self.ty(field.ty()); + if let Some(init) = field.init() { + self.field_init(init); + } + } + for item in def.items(self.db) { + self.contract_item(*item); + } + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) => {} + Item::Error { span, .. } => self.push("Item::Error", span), + } + } + + fn contract_item(&mut self, item: ContractItem<'db>) { + match item { + ContractItem::FunctionDef(def) => self.function(def), + ContractItem::TypeAlias(def) => self.ty(def.ty(self.db)), + ContractItem::AdtDef(def) => { + for ctor in def.ctors(self.db) { + self.ty(*ctor.fields.atom()); + } + } + ContractItem::Error { span, .. } => self.push("ContractItem::Error", span), + } + } + + fn function(&mut self, def: FunctionDef<'db>) { + self.sig(def.sig(self.db)); + if let Some(body) = def.body(self.db) { + self.body(body); + } + } + + fn sig(&mut self, sig: &FuncSig<'db>) { + for pred in &sig.preds { + self.pred(*pred); + } + for param in sig.params.atom() { + self.param(param); + } + if let Some(ret) = sig.ret { + self.ty(ret); + } + } + + fn param(&mut self, param: &FuncParam<'db>) { + match param { + FuncParam::Typed { ty, .. } => self.ty(*ty), + FuncParam::Untyped { .. } => {} + FuncParam::Error { span } => self.push("FuncParam::Error", *span), + } + } + + fn pred(&mut self, pred: PredRef<'db>) { + let kind = pred.kind(self.db); + self.ty(kind.ty); + for arg in kind.args.atom() { + self.ty(*arg); + } + } + + fn ty(&mut self, ty: TypeRef<'db>) { + if !self.seen_types.insert(ty) { + return; + } + match ty.kind(self.db) { + TypeRefKind::Named { args, .. } | TypeRefKind::Tuple { elems: args } => { + for arg in args.atom() { + self.ty(*arg); + } + } + TypeRefKind::FixedArray { element, .. } => self.ty(*element), + TypeRefKind::Fn { params, ret, .. } => { + for param in params.atom() { + self.ty(*param); + } + self.ty(*ret); + } + TypeRefKind::Comptime { inner, .. } => self.ty(*inner), + TypeRefKind::Error { span } => self.push("TypeRefKind::Error", *span), + } + } + + fn body(&mut self, body: FuncBody<'db>) { + for (_, stmt) in body.stmts(self.db).iter() { + self.stmt(stmt); + } + for (_, expr) in body.exprs(self.db).iter() { + self.expr(expr); + } + for (_, pat) in body.pats(self.db).iter() { + self.pat(pat); + } + } + + fn field_init(&mut self, init: &crate::ast::item::FieldInit<'db>) { + for (_, expr) in init.exprs.iter() { + self.expr(expr); + } + } + + fn stmt(&mut self, stmt: &Stmt<'db>) { + match &stmt.kind { + StmtKind::Let { ty: Some(ty), .. } => self.ty(*ty), + StmtKind::Assembly { body } => { + for stmt in body { + self.yul_stmt(stmt); + } + } + StmtKind::Error => self.push("StmtKind::Error", stmt.span), + _ => {} + } + } + + fn expr(&mut self, expr: &Expr<'db>) { + match &expr.kind { + ExprKind::Lit(LitKind::Error) => self.push("LitKind::Error", expr.span), + ExprKind::Proxy { ty, .. } + | ExprKind::Conversion { ty, .. } + | ExprKind::TypeAscription { ty, .. } => self.ty(*ty), + ExprKind::Lambda { params, ret, body } => { + for param in params.atom() { + self.param(param); + } + if let Some(ret) = ret { + self.ty(*ret); + } + self.body(*body); + } + ExprKind::BinOp { op, .. } if *op.atom() == BinOp::Error => { + self.push("BinOp::Error", op.span(self.db)); + } + ExprKind::UnaryOp { op, .. } if *op.atom() == UnOp::Error => { + self.push("UnOp::Error", op.span(self.db)); + } + ExprKind::Error => self.push("ExprKind::Error", expr.span), + _ => {} + } + } + + fn pat(&mut self, pat: &Pat<'db>) { + match &pat.kind { + PatKind::Lit(LitKind::Error) => self.push("LitKind::Error", pat.span), + PatKind::Error => self.push("PatKind::Error", pat.span), + _ => {} + } + } + + fn yul_stmt(&mut self, stmt: &YulStmt<'db>) { + match &stmt.kind { + YulStmtKind::Block(body) | YulStmtKind::FunctionDef { body, .. } => { + self.yul_stmts(body); + } + YulStmtKind::Let { init, .. } => { + if let Some(init) = init { + self.yul_expr(init); + } + } + YulStmtKind::Assign { value, .. } | YulStmtKind::Expr(value) => self.yul_expr(value), + YulStmtKind::If { cond, body } => { + self.yul_expr(cond); + self.yul_stmts(body); + } + YulStmtKind::For { + init, + cond, + post, + body, + } => { + self.yul_stmts(init); + self.yul_expr(cond); + self.yul_stmts(post); + self.yul_stmts(body); + } + YulStmtKind::Switch { + expr, + cases, + default, + } => { + self.yul_expr(expr); + for case in cases { + self.yul_case(case); + } + if let Some(default) = default { + self.yul_stmts(default); + } + } + YulStmtKind::Error => self.push("YulStmtKind::Error", stmt.span), + YulStmtKind::Leave | YulStmtKind::Break | YulStmtKind::Continue => {} + } + } + + fn yul_stmts(&mut self, stmts: &[YulStmt<'db>]) { + for stmt in stmts { + self.yul_stmt(stmt); + } + } + + fn yul_case(&mut self, case: &YulCase<'db>) { + if matches!(case.lit, YulLitKind::Error) { + self.push("YulLitKind::Error", case.span); + } + self.yul_stmts(&case.body); + } + + fn yul_expr(&mut self, expr: &YulExpr<'db>) { + match &expr.kind { + YulExprKind::Lit(YulLitKind::Error) => self.push("YulLitKind::Error", expr.span), + YulExprKind::Call { args, .. } => { + for arg in args { + self.yul_expr(arg); + } + } + YulExprKind::Error => self.push("YulExprKind::Error", expr.span), + _ => {} + } + } +} diff --git a/crates/hull/Cargo.toml b/crates/hull/Cargo.toml new file mode 100644 index 00000000..bc885503 --- /dev/null +++ b/crates/hull/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "solcore-hull" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +hir = { workspace = true } +hir-ty = { workspace = true } +nameres = { workspace = true } +parser = { workspace = true } +specialize = { path = "../specialize", package = "solcore-specialize" } + +[dev-dependencies] +rustc-hash = { workspace = true } +salsa = { workspace = true } +url = { workspace = true } diff --git a/crates/hull/src/check.rs b/crates/hull/src/check.rs new file mode 100644 index 00000000..3ed1bea8 --- /dev/null +++ b/crates/hull/src/check.rs @@ -0,0 +1,1243 @@ +use std::{collections::BTreeMap, fmt}; + +use hir::{ + Db as HirDb, + ast::{ + Ident, + function::{YulExpr, YulExprKind, YulStmt, YulStmtKind}, + }, + diag::{Diagnostic, DiagnosticCode}, + span::{Span, SpannedElem}, +}; + +use crate::{ + ir::{ + Alt, Con, Expr, ExprKind, Function, Object, Pat, PatKind, Program, Stmt, StmtKind, Ty, + TyKind, + }, + scope_stack::ScopeStack, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CheckDiagnostic<'db> { + pub span: Span<'db>, + pub kind: CheckDiagnosticKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckDiagnosticKind { + UndefinedVariable { + name: String, + }, + UndefinedFunction { + name: String, + }, + DuplicateFunction { + name: String, + }, + ArityMismatch { + name: String, + expected: usize, + actual: usize, + }, + TypeMismatch { + expected: String, + actual: String, + }, + ExprAnnotationMismatch { + annotated: String, + inferred: String, + }, + ExpectedProduct { + actual: String, + }, + ExpectedSum { + actual: String, + }, + ExpectedBool { + actual: String, + }, + BadInjectionIndex { + index: usize, + ty: String, + }, + BadMatchPattern { + pat: String, + ty: String, + }, + ReturnOutsideFunction, + FunctionTypeNotFirstOrder { + name: String, + }, + MissingTerminator { + function: String, + }, + AssemblyRequiresDatabase, + AssemblyReturnCountMismatch { + context: String, + expected: usize, + actual: usize, + }, + AssemblyExpressionNotUnit { + actual: String, + }, + AssemblyExpectedWordArgument { + actual: String, + }, + AssemblyExpectedWordAssignment { + name: String, + actual: String, + }, + AssemblyVoidArgument, +} + +impl<'db> CheckDiagnostic<'db> { + pub fn lower(&self, db: &'db dyn HirDb) -> Diagnostic { + Diagnostic::error(self.kind.to_string()) + .with_code(self.kind.code()) + .with_primary_label(db, self.span, Some(self.kind.primary_label())) + } +} + +impl CheckDiagnosticKind { + pub fn code(&self) -> &'static str { + match self { + Self::UndefinedVariable { .. } => DiagnosticCode::HULL_UNDEFINED_VARIABLE, + Self::UndefinedFunction { .. } => DiagnosticCode::HULL_UNDEFINED_FUNCTION, + Self::DuplicateFunction { .. } => DiagnosticCode::HULL_DUPLICATE_FUNCTION, + Self::ArityMismatch { .. } => DiagnosticCode::HULL_ARITY_MISMATCH, + Self::TypeMismatch { .. } => DiagnosticCode::HULL_TYPE_MISMATCH, + Self::ExprAnnotationMismatch { .. } => DiagnosticCode::HULL_EXPR_ANNOTATION_MISMATCH, + Self::ExpectedProduct { .. } => DiagnosticCode::HULL_EXPECTED_PRODUCT, + Self::ExpectedSum { .. } => DiagnosticCode::HULL_EXPECTED_SUM, + Self::ExpectedBool { .. } => DiagnosticCode::HULL_EXPECTED_BOOL, + Self::BadInjectionIndex { .. } => DiagnosticCode::HULL_BAD_INJECTION_INDEX, + Self::BadMatchPattern { .. } => DiagnosticCode::HULL_BAD_MATCH_PATTERN, + Self::ReturnOutsideFunction => DiagnosticCode::HULL_RETURN_OUTSIDE_FUNCTION, + Self::FunctionTypeNotFirstOrder { .. } => { + DiagnosticCode::HULL_FUNCTION_TYPE_NOT_FIRST_ORDER + } + Self::MissingTerminator { .. } => DiagnosticCode::HULL_MISSING_TERMINATOR, + Self::AssemblyRequiresDatabase => DiagnosticCode::HULL_ASSEMBLY_REQUIRES_DATABASE, + Self::AssemblyReturnCountMismatch { .. } => { + DiagnosticCode::HULL_ASSEMBLY_RETURN_COUNT_MISMATCH + } + Self::AssemblyExpressionNotUnit { .. } => { + DiagnosticCode::HULL_ASSEMBLY_EXPRESSION_NOT_UNIT + } + Self::AssemblyExpectedWordArgument { .. } => { + DiagnosticCode::HULL_ASSEMBLY_EXPECTED_WORD_ARGUMENT + } + Self::AssemblyExpectedWordAssignment { .. } => { + DiagnosticCode::HULL_ASSEMBLY_EXPECTED_WORD_ASSIGNMENT + } + Self::AssemblyVoidArgument => DiagnosticCode::HULL_ASSEMBLY_VOID_ARGUMENT, + } + } + + fn primary_label(&self) -> &'static str { + match self { + Self::UndefinedVariable { .. } => "undefined variable", + Self::UndefinedFunction { .. } => "undefined function", + Self::DuplicateFunction { .. } => "duplicate function", + Self::ArityMismatch { .. } => "wrong number of arguments", + Self::TypeMismatch { .. } => "type mismatch", + Self::ExprAnnotationMismatch { .. } => "annotation mismatch", + Self::ExpectedProduct { .. } => "product value required", + Self::ExpectedSum { .. } => "sum value required", + Self::ExpectedBool { .. } => "boolean value required", + Self::BadInjectionIndex { .. } => "bad injection index", + Self::BadMatchPattern { .. } => "bad match pattern", + Self::ReturnOutsideFunction => "return outside function", + Self::FunctionTypeNotFirstOrder { .. } => "function type is not first-order", + Self::MissingTerminator { .. } => "missing terminator", + Self::AssemblyRequiresDatabase => "database required for assembly check", + Self::AssemblyReturnCountMismatch { .. } => "assembly return count mismatch", + Self::AssemblyExpressionNotUnit { .. } => "assembly expression must be unit", + Self::AssemblyExpectedWordArgument { .. } => "assembly argument must be word", + Self::AssemblyExpectedWordAssignment { .. } => "assembly assignment must be word", + Self::AssemblyVoidArgument => "assembly argument has no value", + } + } +} + +impl fmt::Display for CheckDiagnosticKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UndefinedVariable { name } => write!(f, "undefined Hull variable `{name}`"), + Self::UndefinedFunction { name } => write!(f, "undefined Hull function `{name}`"), + Self::DuplicateFunction { name } => write!(f, "duplicate Hull function `{name}`"), + Self::ArityMismatch { + name, + expected, + actual, + } => write!( + f, + "wrong arity for Hull function `{name}`: expected {expected}, got {actual}" + ), + Self::TypeMismatch { expected, actual } => { + write!(f, "Hull type mismatch: expected {expected}, got {actual}") + } + Self::ExprAnnotationMismatch { + annotated, + inferred, + } => write!( + f, + "Hull expression annotation mismatch: annotated {annotated}, inferred {inferred}" + ), + Self::ExpectedProduct { actual } => write!(f, "expected Hull product, got {actual}"), + Self::ExpectedSum { actual } => write!(f, "expected Hull sum, got {actual}"), + Self::ExpectedBool { actual } => write!(f, "expected Hull bool, got {actual}"), + Self::BadInjectionIndex { index, ty } => { + write!(f, "bad Hull injection index {index} for {ty}") + } + Self::BadMatchPattern { pat, ty } => { + write!(f, "Hull pattern {pat} does not match {ty}") + } + Self::ReturnOutsideFunction => write!(f, "Hull return appears outside a function"), + Self::FunctionTypeNotFirstOrder { name } => { + write!(f, "Hull function `{name}` has a non-first-order type") + } + Self::MissingTerminator { function } => { + write!(f, "Hull function `{function}` is missing a terminator") + } + Self::AssemblyRequiresDatabase => { + write!(f, "cannot check inline assembly without a source database") + } + Self::AssemblyReturnCountMismatch { + context, + expected, + actual, + } => write!( + f, + "inline assembly {context} returns {actual} values, expected {expected}" + ), + Self::AssemblyExpressionNotUnit { actual } => { + write!( + f, + "inline assembly expression must have unit type, got {actual}" + ) + } + Self::AssemblyExpectedWordArgument { actual } => { + write!( + f, + "inline assembly argument must have word type, got {actual}" + ) + } + Self::AssemblyExpectedWordAssignment { name, actual } => write!( + f, + "inline assembly assignment to `{name}` requires word type, got {actual}" + ), + Self::AssemblyVoidArgument => { + write!(f, "inline assembly argument does not produce a value") + } + } + } +} + +#[derive(Debug, Clone)] +struct FunSig<'db> { + args: Vec>, + ret: Ty<'db>, +} + +struct Env<'db> { + db: Option<&'db dyn HirDb>, + vars: ScopeStack>>, + funs: BTreeMap>, + ret: Option>, + diagnostics: Vec>, +} + +pub fn check_program<'db>(program: &Program<'db>) -> Vec> { + check_program_inner(None, program) +} + +pub fn check_program_with_db<'db>( + db: &'db dyn HirDb, + program: &Program<'db>, +) -> Vec> { + check_program_inner(Some(db), program) +} + +fn check_program_inner<'db>( + db: Option<&'db dyn HirDb>, + program: &Program<'db>, +) -> Vec> { + let mut env = Env { + db, + vars: ScopeStack::new_root(BTreeMap::new()), + funs: builtin_funs(program.span), + ret: None, + diagnostics: Vec::new(), + }; + for function in &program.functions { + env.register_function(function); + } + for function in &program.functions { + env.check_function(function); + } + for object in &program.objects { + env.check_object(object); + } + env.diagnostics +} + +impl<'db> Env<'db> { + fn register_function(&mut self, function: &Function<'db>) { + if self.funs.contains_key(function.name.as_str()) { + self.push( + function.span, + CheckDiagnosticKind::DuplicateFunction { + name: function.name.as_str().to_owned(), + }, + ); + } + self.funs.insert( + function.name.as_str().to_owned(), + FunSig { + args: function.args.iter().map(|arg| arg.ty.clone()).collect(), + ret: function.ret.clone(), + }, + ); + } + + fn check_object(&mut self, object: &Object<'db>) { + let saved_funs = self.funs.clone(); + for function in &object.code.functions { + self.register_function(function); + } + self.with_scope(|env| { + for function in &object.code.functions { + env.check_function(function); + } + env.check_body(&object.code.stmts); + }); + // Yul object scoping: an inner object's code does not see the outer + // object's functions, so restore before recursing. + self.funs = saved_funs; + for inner in &object.inners { + self.check_object(inner); + } + } + + fn check_function(&mut self, function: &Function<'db>) { + for arg in &function.args { + if arg.ty.contains_function() { + self.push( + arg.span, + CheckDiagnosticKind::FunctionTypeNotFirstOrder { + name: function.name.as_str().to_owned(), + }, + ); + } + } + if function.ret.contains_function() { + self.push( + function.ret.span, + CheckDiagnosticKind::FunctionTypeNotFirstOrder { + name: function.name.as_str().to_owned(), + }, + ); + } + self.with_scope(|env| { + for arg in &function.args { + env.insert_var(arg.name.as_str().to_owned(), arg.ty.clone()); + } + let saved_ret = env.ret.clone(); + env.ret = Some(function.ret.clone()); + env.check_body(&function.body); + if requires_terminator(&function.ret) && !body_terminates(&function.body, env.db) { + env.push( + function.span, + CheckDiagnosticKind::MissingTerminator { + function: function.name.as_str().to_owned(), + }, + ); + } + env.ret = saved_ret; + }); + } + + fn check_body(&mut self, body: &[Stmt<'db>]) { + for stmt in body { + self.check_stmt(stmt); + } + } + + fn check_stmt(&mut self, stmt: &Stmt<'db>) { + match &stmt.kind { + StmtKind::Let { name, ty } => self.insert_var(name.as_str().to_owned(), ty.clone()), + StmtKind::Assign { lhs, rhs } => { + let lhs_ty = self.check_expr(lhs); + let rhs_ty = self.check_expr(rhs); + self.expect_type(lhs.span, &lhs_ty, &rhs_ty); + } + StmtKind::Expr(expr) => { + self.check_expr(expr); + } + StmtKind::Return(expr) => { + let actual = self.check_expr(expr); + match self.ret.clone() { + Some(expected) => self.expect_type(expr.span, &expected, &actual), + None => self.push(expr.span, CheckDiagnosticKind::ReturnOutsideFunction), + } + } + StmtKind::Block(stmts) => self.with_scope(|env| env.check_body(stmts)), + StmtKind::For { + init, + cond, + post, + body, + } => self.with_scope(|env| { + env.check_body(init); + let cond_ty = env.check_expr(cond); + if !is_bool_like(&cond_ty) { + env.push( + cond.span, + CheckDiagnosticKind::ExpectedBool { + actual: ty_display(&cond_ty), + }, + ); + } + env.check_body(post); + env.check_body(body); + }), + StmtKind::Break | StmtKind::Continue => {} + StmtKind::Match { + target, + scrutinee, + alts, + } => { + let scrutinee_ty = self.check_expr(scrutinee); + self.expect_type(scrutinee.span, target, &scrutinee_ty); + for alt in alts { + self.check_alt(target, alt); + } + } + StmtKind::Assembly(stmts) => { + if self.db.is_some() { + self.with_scope(|env| env.check_asm_block(stmts)); + } else { + self.push(stmt.span, CheckDiagnosticKind::AssemblyRequiresDatabase); + } + } + StmtKind::Revert(_) | StmtKind::Comment(_) => {} + } + } + + fn check_alt(&mut self, target: &Ty<'db>, alt: &Alt<'db>) { + let payload = match payload_type(target, &alt.pat) { + Some(payload) => payload, + None => { + self.push( + alt.span, + CheckDiagnosticKind::BadMatchPattern { + pat: pat_display(&alt.pat), + ty: ty_display(target), + }, + ); + Ty::unit(alt.span) + } + }; + self.with_scope(|env| { + env.insert_var(alt.binder.as_str().to_owned(), payload); + env.check_body(&alt.body); + }); + } + + fn check_expr(&mut self, expr: &Expr<'db>) -> Ty<'db> { + let inferred = self.infer_expr(expr); + if !type_eq(&expr.ty, &inferred) { + self.push( + expr.span, + CheckDiagnosticKind::ExprAnnotationMismatch { + annotated: ty_display(&expr.ty), + inferred: ty_display(&inferred), + }, + ); + } + inferred + } + + fn infer_expr(&mut self, expr: &Expr<'db>) -> Ty<'db> { + match &expr.kind { + ExprKind::Word(_) => Ty::word(expr.span), + ExprKind::Bool(_) => Ty::bool(expr.span), + ExprKind::Unit => Ty::unit(expr.span), + ExprKind::Var(name) => self.lookup_var(name.as_str()).unwrap_or_else(|| { + self.push( + expr.span, + CheckDiagnosticKind::UndefinedVariable { + name: name.as_str().to_owned(), + }, + ); + expr.ty.clone() + }), + ExprKind::Pair(lhs, rhs) => { + let lhs_ty = self.check_expr(lhs); + let rhs_ty = self.check_expr(rhs); + Ty::product(expr.span, lhs_ty, rhs_ty) + } + ExprKind::Fst(inner) => { + let actual = self.check_expr(inner); + match actual.strip_named().kind.clone() { + TyKind::Product(lhs, _) => *lhs, + _ => { + self.push( + inner.span, + CheckDiagnosticKind::ExpectedProduct { + actual: ty_display(&actual), + }, + ); + expr.ty.clone() + } + } + } + ExprKind::Snd(inner) => { + let actual = self.check_expr(inner); + match actual.strip_named().kind.clone() { + TyKind::Product(_, rhs) => *rhs, + _ => { + self.push( + inner.span, + CheckDiagnosticKind::ExpectedProduct { + actual: ty_display(&actual), + }, + ); + expr.ty.clone() + } + } + } + ExprKind::Inl { target, value } => { + match target.strip_named().kind.clone() { + TyKind::Sum(lhs, _) => { + let actual = self.check_expr(value); + self.expect_type(value.span, &lhs, &actual); + } + _ => self.push( + target.span, + CheckDiagnosticKind::ExpectedSum { + actual: ty_display(target), + }, + ), + } + target.clone() + } + ExprKind::Inr { target, value } => { + match target.strip_named().kind.clone() { + TyKind::Sum(_, rhs) => { + let actual = self.check_expr(value); + self.expect_type(value.span, &rhs, &actual); + } + _ => self.push( + target.span, + CheckDiagnosticKind::ExpectedSum { + actual: ty_display(target), + }, + ), + } + target.clone() + } + ExprKind::InK { + index, + target, + value, + } => { + match nth_sum_payload(target, *index) { + Some(expected) => { + let actual = self.check_expr(value); + self.expect_type(value.span, &expected, &actual); + } + None => self.push( + target.span, + CheckDiagnosticKind::BadInjectionIndex { + index: *index, + ty: ty_display(target), + }, + ), + } + target.clone() + } + ExprKind::Call { callee, args } => { + let Some(sig) = self.funs.get(callee.as_str()).cloned() else { + self.push( + expr.span, + CheckDiagnosticKind::UndefinedFunction { + name: callee.as_str().to_owned(), + }, + ); + return expr.ty.clone(); + }; + if sig.args.len() != args.len() { + self.push( + expr.span, + CheckDiagnosticKind::ArityMismatch { + name: callee.as_str().to_owned(), + expected: sig.args.len(), + actual: args.len(), + }, + ); + return sig.ret; + } + for (expected, arg) in sig.args.iter().zip(args) { + let actual = self.check_expr(arg); + self.expect_type(arg.span, expected, &actual); + } + sig.ret + } + ExprKind::If { + target, + cond, + then_expr, + else_expr, + } => { + let cond_ty = self.check_expr(cond); + if !is_bool_like(&cond_ty) { + self.push( + cond.span, + CheckDiagnosticKind::ExpectedBool { + actual: ty_display(&cond_ty), + }, + ); + } + let then_ty = self.check_expr(then_expr); + let else_ty = self.check_expr(else_expr); + self.expect_type(then_expr.span, target, &then_ty); + self.expect_type(else_expr.span, target, &else_ty); + target.clone() + } + } + } + + fn check_asm_block(&mut self, stmts: &[YulStmt<'db>]) { + for stmt in stmts { + self.check_asm_stmt(stmt); + } + } + + fn check_asm_stmt(&mut self, stmt: &YulStmt<'db>) { + match &stmt.kind { + YulStmtKind::Block(stmts) => self.with_scope(|env| env.check_asm_block(stmts)), + YulStmtKind::Let { names, init } => { + if let Some(init) = init { + let ty = self.check_asm_expr(init); + let expected = names.len(); + let actual = return_count(&ty); + if actual != expected { + self.push( + init.span, + CheckDiagnosticKind::AssemblyReturnCountMismatch { + context: "let binding".to_owned(), + expected, + actual, + }, + ); + } + } + for name in names { + self.insert_var(self.yul_name(name), Ty::word(stmt.span)); + } + } + YulStmtKind::Assign { names, value } => { + let mut expected = 0usize; + for name in names { + let name_text = self.yul_name(name); + match self.lookup_var(&name_text) { + Some(ty) => { + if !is_word_type(&ty) { + self.push( + stmt.span, + CheckDiagnosticKind::AssemblyExpectedWordAssignment { + name: name_text, + actual: ty_display(&ty), + }, + ); + } + } + None => self.push( + stmt.span, + CheckDiagnosticKind::UndefinedVariable { name: name_text }, + ), + } + expected += 1; + } + let actual_ty = self.check_asm_expr(value); + let actual = return_count(&actual_ty); + if actual != expected { + self.push( + value.span, + CheckDiagnosticKind::AssemblyReturnCountMismatch { + context: "assignment".to_owned(), + expected, + actual, + }, + ); + } + } + YulStmtKind::Expr(expr) => { + let ty = self.check_asm_expr(expr); + if !type_eq(&ty, &Ty::unit(expr.span)) { + self.push( + expr.span, + CheckDiagnosticKind::AssemblyExpressionNotUnit { + actual: ty_display(&ty), + }, + ); + } + } + YulStmtKind::If { cond, body } => { + self.check_asm_arg(cond); + self.check_asm_block(body); + } + YulStmtKind::For { + init, + cond, + post, + body, + } => self.with_scope(|env| { + env.check_asm_block(init); + env.check_asm_arg(cond); + env.check_asm_block(post); + env.check_asm_block(body); + }), + YulStmtKind::Switch { + expr, + cases, + default, + } => { + self.check_asm_arg(expr); + for case in cases { + self.check_asm_block(&case.body); + } + if let Some(default) = default { + self.check_asm_block(default); + } + } + YulStmtKind::FunctionDef { + name, + params, + rets, + body, + } => { + let fun_name = self.yul_name(name); + self.funs.insert( + fun_name, + FunSig { + args: vec![Ty::word(stmt.span); params.len()], + ret: n_returns(stmt.span, rets.len()), + }, + ); + self.with_scope(|env| { + for param in params { + env.insert_var(env.yul_name(param), Ty::word(stmt.span)); + } + for ret in rets { + env.insert_var(env.yul_name(ret), Ty::word(stmt.span)); + } + env.check_asm_block(body); + }); + } + YulStmtKind::Leave + | YulStmtKind::Break + | YulStmtKind::Continue + | YulStmtKind::Error => {} + } + } + + fn check_asm_expr(&mut self, expr: &YulExpr<'db>) -> Ty<'db> { + match &expr.kind { + YulExprKind::Lit(_) => Ty::word(expr.span), + YulExprKind::Ident(name) => { + let name = self.yul_name(name); + self.lookup_var(&name).unwrap_or_else(|| { + self.push(expr.span, CheckDiagnosticKind::UndefinedVariable { name }); + Ty::word(expr.span) + }) + } + YulExprKind::Call { name, args } => { + let name = self.yul_name(name); + let sig = self.lookup_asm_fun(expr.span, &name); + if sig.args.len() != args.len() { + self.push( + expr.span, + CheckDiagnosticKind::ArityMismatch { + name, + expected: sig.args.len(), + actual: args.len(), + }, + ); + return sig.ret; + } + for arg in args { + self.check_asm_arg(arg); + } + sig.ret + } + YulExprKind::Error => Ty::word(expr.span), + } + } + + fn check_asm_arg(&mut self, expr: &YulExpr<'db>) { + let ty = self.check_asm_expr(expr); + if is_word_type(&ty) { + return; + } + if type_eq(&ty, &Ty::unit(expr.span)) { + self.push(expr.span, CheckDiagnosticKind::AssemblyVoidArgument); + } else { + self.push( + expr.span, + CheckDiagnosticKind::AssemblyExpectedWordArgument { + actual: ty_display(&ty), + }, + ); + } + } + + fn lookup_asm_fun(&mut self, span: Span<'db>, name: &str) -> FunSig<'db> { + if let Some(sig) = asm_builtin_sig(span, name) { + return sig; + } + let key = name.strip_prefix("usr$").unwrap_or(name); + match self.funs.get(key).cloned() { + Some(sig) => FunSig { + args: vec![Ty::word(span); sig.args.len()], + ret: n_returns(span, return_count(&sig.ret)), + }, + None => { + self.push( + span, + CheckDiagnosticKind::UndefinedFunction { + name: name.to_owned(), + }, + ); + FunSig { + args: Vec::new(), + ret: Ty::unit(span), + } + } + } + } + + fn yul_name(&self, name: &SpannedElem<'db, Ident<'db>>) -> String { + if let Some(db) = self.db { + (*name.atom()).text(db).to_owned() + } else { + "".to_owned() + } + } + + fn expect_type(&mut self, span: Span<'db>, expected: &Ty<'db>, actual: &Ty<'db>) { + if !type_eq(expected, actual) { + self.push( + span, + CheckDiagnosticKind::TypeMismatch { + expected: ty_display(expected), + actual: ty_display(actual), + }, + ); + } + } + + fn insert_var(&mut self, name: String, ty: Ty<'db>) { + self.vars.last_mut().insert(name, ty); + } + + fn lookup_var(&self, name: &str) -> Option> { + self.vars + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + } + + fn with_scope(&mut self, f: impl FnOnce(&mut Self)) { + self.vars.push(BTreeMap::new()); + f(self); + let _ = self.vars.pop(); + } + + fn push(&mut self, span: Span<'db>, kind: CheckDiagnosticKind) { + self.diagnostics.push(CheckDiagnostic { span, kind }); + } +} + +fn builtin_funs<'db>(span: Span<'db>) -> BTreeMap> { + let word = Ty::word(span); + let unit = Ty::unit(span); + let bool_sum = bool_sum_ty(span); + let mut funs = BTreeMap::new(); + let mut add = |name: &str, args: Vec>, ret: Ty<'db>| { + funs.insert(name.to_owned(), FunSig { args, ret }); + }; + for name in [ + "add", + "sub", + "mul", + "div", + "sdiv", + "mod", + "smod", + "exp", + "signextend", + "and", + "or", + "xor", + "byte", + "shl", + "shr", + "sar", + "keccak256", + "primAddWord", + "subWord", + "mulWord", + "bxorWord", + "bandWord", + "borWord", + "integerAdd", + "integerSub", + "integerMul", + "wordFromInteger", + ] { + let argc = if name == "wordFromInteger" { 1 } else { 2 }; + add(name, vec![word.clone(); argc], word.clone()); + } + for name in ["addmod", "mulmod"] { + add(name, vec![word.clone(); 3], word.clone()); + } + for name in [ + "mload", + "sload", + "tload", + "calldataload", + "memoryguard", + "balance", + "extcodesize", + "extcodehash", + "blockhash", + "blobhash", + ] { + add(name, vec![word.clone()], word.clone()); + } + for name in [ + "address", + "origin", + "caller", + "callvalue", + "calldatasize", + "codesize", + "gasprice", + "returndatasize", + "coinbase", + "timestamp", + "number", + "prevrandao", + "gaslimit", + "chainid", + "selfbalance", + "basefee", + "blobbasefee", + "msize", + "gas", + ] { + add(name, Vec::new(), word.clone()); + } + for name in [ + "lt", + "gt", + "slt", + "sgt", + "eq", + "primEqWord", + "gtWord", + "integerLt", + "integerEq", + ] { + add(name, vec![word.clone(), word.clone()], bool_sum.clone()); + } + add("iszero", vec![bool_sum.clone()], bool_sum.clone()); + for name in ["not", "clz", "wordToInteger"] { + add(name, vec![word.clone()], word.clone()); + } + for name in [ + "stop", + "invalid", + "mstore", + "mstore8", + "sstore", + "tstore", + "return", + "revert", + "pop", + "selfdestruct", + "calldatacopy", + "codecopy", + "returndatacopy", + "mcopy", + "datacopy", + ] { + let argc = match name { + "stop" | "invalid" => 0, + "pop" | "selfdestruct" => 1, + "calldatacopy" | "codecopy" | "returndatacopy" | "mcopy" | "datacopy" => 3, + _ => 2, + }; + add(name, vec![word.clone(); argc], unit.clone()); + } + add("extcodecopy", vec![word.clone(); 4], unit.clone()); + add("create", vec![word.clone(); 3], word.clone()); + add("create2", vec![word.clone(); 4], word.clone()); + add("call", vec![word.clone(); 7], word.clone()); + add("callcode", vec![word.clone(); 7], word.clone()); + add("delegatecall", vec![word.clone(); 6], word.clone()); + add("staticcall", vec![word.clone(); 6], word.clone()); + for index in 0..=4 { + add( + &format!("log{index}"), + vec![word.clone(); 2 + index], + unit.clone(), + ); + } + for name in ["dataoffset", "datasize", "loadimmutable", "linkersymbol"] { + add(name, vec![word.clone()], word.clone()); + } + add( + "setimmutable", + vec![word.clone(), word.clone(), word.clone()], + unit.clone(), + ); + funs +} + +fn asm_builtin_sig<'db>(span: Span<'db>, name: &str) -> Option> { + let word = Ty::word(span); + let unit = Ty::unit(span); + let sig = |args: usize, ret: Ty<'db>| FunSig { + args: vec![word.clone(); args], + ret, + }; + let fun = match name { + "stop" | "invalid" => sig(0, unit.clone()), + "add" | "sub" | "mul" | "div" | "sdiv" | "mod" | "smod" | "exp" | "signextend" | "lt" + | "gt" | "slt" | "sgt" | "eq" | "and" | "or" | "xor" | "byte" | "shl" | "shr" | "sar" + | "keccak256" => sig(2, word.clone()), + "addmod" | "mulmod" => sig(3, word.clone()), + "iszero" | "not" | "clz" | "balance" | "calldataload" | "extcodesize" | "extcodehash" + | "blockhash" | "blobhash" | "mload" | "sload" | "tload" => sig(1, word.clone()), + "pop" | "selfdestruct" => sig(1, unit.clone()), + "address" | "origin" | "caller" | "callvalue" | "calldatasize" | "codesize" + | "gasprice" | "returndatasize" | "coinbase" | "timestamp" | "number" | "prevrandao" + | "gaslimit" | "chainid" | "selfbalance" | "basefee" | "blobbasefee" | "msize" | "gas" => { + sig(0, word.clone()) + } + "mstore" | "mstore8" | "sstore" | "tstore" | "return" | "revert" => sig(2, unit.clone()), + "calldatacopy" | "codecopy" | "returndatacopy" | "mcopy" | "datacopy" => { + sig(3, unit.clone()) + } + "extcodecopy" => sig(4, unit.clone()), + "create" => sig(3, word.clone()), + "create2" => sig(4, word.clone()), + "call" | "callcode" => sig(7, word.clone()), + "delegatecall" | "staticcall" => sig(6, word.clone()), + "log0" => sig(2, unit.clone()), + "log1" => sig(3, unit.clone()), + "log2" => sig(4, unit.clone()), + "log3" => sig(5, unit.clone()), + "log4" => sig(6, unit.clone()), + "memoryguard" | "dataoffset" | "datasize" | "loadimmutable" | "linkersymbol" => { + sig(1, word.clone()) + } + "setimmutable" => sig(3, unit.clone()), + _ => return None, + }; + Some(fun) +} + +fn payload_type<'db>(target: &Ty<'db>, pat: &Pat<'db>) -> Option> { + match (&target.strip_named().kind, &pat.kind) { + (TyKind::Sum(lhs, _), PatKind::Con(Con::Inl)) => Some((**lhs).clone()), + (TyKind::Sum(_, rhs), PatKind::Con(Con::Inr)) => Some((**rhs).clone()), + (_, PatKind::Con(Con::InK(index))) => nth_sum_payload(target, *index), + (_, PatKind::Wildcard | PatKind::Var(_)) => Some(target.clone()), + (TyKind::Word, PatKind::IntLit(_)) => Some(Ty::word(pat.span)), + _ => None, + } +} + +fn nth_sum_payload<'db>(target: &Ty<'db>, index: usize) -> Option> { + let mut current = target.strip_named(); + let mut remaining = index; + loop { + match ¤t.strip_named().kind { + TyKind::Sum(lhs, _) if remaining == 0 => return Some((**lhs).clone()), + TyKind::Sum(_, rhs) => { + current = rhs.strip_named(); + remaining -= 1; + } + _ if remaining == 0 => return Some(current.clone()), + _ => return None, + } + } +} + +fn bool_sum_ty<'db>(span: Span<'db>) -> Ty<'db> { + Ty::sum(span, Ty::unit(span), Ty::unit(span)) +} + +fn is_bool_like(ty: &Ty<'_>) -> bool { + matches!(ty.strip_named().kind, TyKind::Bool) + || matches!( + &ty.strip_named().kind, + TyKind::Sum(lhs, rhs) + if matches!(lhs.strip_named().kind, TyKind::Unit) + && matches!(rhs.strip_named().kind, TyKind::Unit) + ) +} + +fn is_word_type(ty: &Ty<'_>) -> bool { + matches!(ty.strip_named().kind, TyKind::Word) +} + +fn return_count(ty: &Ty<'_>) -> usize { + match &ty.strip_named().kind { + TyKind::Unit => 0, + TyKind::Word | TyKind::Bool => 1, + TyKind::Product(lhs, rhs) => return_count(lhs) + return_count(rhs), + TyKind::Sum(lhs, rhs) => 1 + return_count(lhs).max(return_count(rhs)), + TyKind::Named { inner, .. } => return_count(inner), + TyKind::NamedRef { .. } => 1, + TyKind::Function { .. } => 1, + } +} + +fn n_returns<'db>(span: Span<'db>, count: usize) -> Ty<'db> { + match count { + 0 => Ty::unit(span), + 1 => Ty::word(span), + _ => Ty::product(span, Ty::word(span), n_returns(span, count - 1)), + } +} + +fn requires_terminator(ty: &Ty<'_>) -> bool { + return_count(ty) > 0 +} + +fn type_eq(lhs: &Ty<'_>, rhs: &Ty<'_>) -> bool { + match (&lhs.kind, &rhs.kind) { + (TyKind::NamedRef { name: a }, TyKind::NamedRef { name: b }) + | (TyKind::NamedRef { name: a }, TyKind::Named { name: b, .. }) + | (TyKind::Named { name: a, .. }, TyKind::NamedRef { name: b }) => { + return a == b; + } + _ => {} + } + + match (&lhs.strip_named().kind, &rhs.strip_named().kind) { + (TyKind::Word, TyKind::Word) + | (TyKind::Bool, TyKind::Bool) + | (TyKind::Unit, TyKind::Unit) => true, + (TyKind::Product(a_lhs, a_rhs), TyKind::Product(b_lhs, b_rhs)) + | (TyKind::Sum(a_lhs, a_rhs), TyKind::Sum(b_lhs, b_rhs)) => { + type_eq(a_lhs, b_lhs) && type_eq(a_rhs, b_rhs) + } + ( + TyKind::Function { + params: a_params, + ret: a_ret, + }, + TyKind::Function { + params: b_params, + ret: b_ret, + }, + ) => { + a_params.len() == b_params.len() + && a_params + .iter() + .zip(b_params) + .all(|(lhs, rhs)| type_eq(lhs, rhs)) + && type_eq(a_ret, b_ret) + } + _ => false, + } +} + +fn body_terminates(body: &[Stmt<'_>], db: Option<&dyn HirDb>) -> bool { + body.last().is_some_and(|stmt| stmt_terminates(stmt, db)) +} + +fn stmt_terminates(stmt: &Stmt<'_>, db: Option<&dyn HirDb>) -> bool { + match &stmt.kind { + StmtKind::Return(_) | StmtKind::Revert(_) => true, + StmtKind::Block(body) => body_terminates(body, db), + StmtKind::Match { alts, .. } => { + !alts.is_empty() && alts.iter().all(|alt| body_terminates(&alt.body, db)) + } + StmtKind::Assembly(stmts) => asm_block_terminates(stmts, db), + StmtKind::Let { .. } + | StmtKind::Assign { .. } + | StmtKind::Expr(_) + | StmtKind::For { .. } + | StmtKind::Break + | StmtKind::Continue + | StmtKind::Comment(_) => false, + } +} + +fn asm_block_terminates(stmts: &[YulStmt<'_>], db: Option<&dyn HirDb>) -> bool { + stmts + .last() + .is_some_and(|stmt| asm_stmt_terminates(stmt, db)) +} + +fn asm_stmt_terminates(stmt: &YulStmt<'_>, db: Option<&dyn HirDb>) -> bool { + match &stmt.kind { + YulStmtKind::Block(stmts) => asm_block_terminates(stmts, db), + YulStmtKind::Expr(YulExpr { + kind: YulExprKind::Call { name, .. }, + .. + }) => db + .map(|db| { + let name = (*name.atom()).text(db); + matches!(name, "return" | "revert") + }) + .unwrap_or(false), + YulStmtKind::Switch { cases, default, .. } => { + !cases.is_empty() + && default.is_some() + && cases + .iter() + .all(|case| asm_block_terminates(&case.body, db)) + && default + .as_ref() + .is_some_and(|body| asm_block_terminates(body, db)) + } + _ => false, + } +} + +fn ty_display(ty: &Ty<'_>) -> String { + match &ty.kind { + TyKind::Word => "word".to_owned(), + TyKind::Bool => "bool".to_owned(), + TyKind::Unit => "unit".to_owned(), + TyKind::Product(lhs, rhs) => format!("({} * {})", ty_display(lhs), ty_display(rhs)), + TyKind::Sum(lhs, rhs) => format!("({} + {})", ty_display(lhs), ty_display(rhs)), + TyKind::Named { name, inner } => format!("{name}{{{}}}", ty_display(inner)), + TyKind::NamedRef { name } => name.as_str().to_owned(), + TyKind::Function { params, ret } => { + let params = params.iter().map(ty_display).collect::>().join(", "); + format!("({params} -> {})", ty_display(ret)) + } + } +} + +fn pat_display(pat: &Pat<'_>) -> String { + match &pat.kind { + PatKind::Var(name) => name.as_str().to_owned(), + PatKind::Con(Con::Inl) => "inl".to_owned(), + PatKind::Con(Con::Inr) => "inr".to_owned(), + PatKind::Con(Con::InK(index)) => format!("in({index})"), + PatKind::Wildcard => "_".to_owned(), + PatKind::IntLit(value) => value.clone(), + } +} diff --git a/crates/hull/src/emit/contract.rs b/crates/hull/src/emit/contract.rs new file mode 100644 index 00000000..bc8d29bf --- /dev/null +++ b/crates/hull/src/emit/contract.rs @@ -0,0 +1,168 @@ +use super::*; + +impl<'db> Emitter<'db> { + pub(super) fn emit_contract( + &mut self, + contract: &MonoContract<'db>, + functions: &[Function<'db>], + ) -> Object<'db> { + let deployment_main = contract.entries.iter().find_map(|entry| { + if let MonoEntry::DeploymentMain { + specialized, span, .. + } = entry + { + Some((specialized, *span)) + } else { + None + } + }); + let runtime_main = contract.entries.iter().find_map(|entry| { + if let MonoEntry::RuntimeMain { + specialized, span, .. + } = entry + { + Some((specialized, *span)) + } else { + None + } + }); + + let storage_fields = self.contract_storage_fields(contract.def); + let storage_hash_helper = storage_fields + .values() + .any(|field| field.kind == StorageFieldKind::Mapping) + .then_some(STORAGE_HASH2_HELPER.to_owned()); + + let deployment_roots = + deployment_main.map_or_else(BTreeSet::new, |(name, _)| BTreeSet::from([name.clone()])); + let runtime_roots = + runtime_main.map_or_else(BTreeSet::new, |(name, _)| BTreeSet::from([name.clone()])); + let deployment_names = deployment_closure(self.db, functions, &deployment_roots); + let runtime_names = deployment_closure(self.db, functions, &runtime_roots); + + let mut mapping_value_helper_used = false; + let mut deployment_functions = functions + .iter() + .filter(|function| deployment_names.contains(function.name.as_str())) + .cloned() + .map(|function| { + self.lower_storage_fields_in_function( + function, + &storage_fields, + storage_hash_helper.as_deref(), + &mut mapping_value_helper_used, + ) + }) + .collect::>(); + let mut runtime_functions = functions + .iter() + .filter(|function| runtime_names.contains(function.name.as_str())) + .cloned() + .map(|function| { + self.lower_storage_fields_in_function( + function, + &storage_fields, + storage_hash_helper.as_deref(), + &mut mapping_value_helper_used, + ) + }) + .collect::>(); + if let Some(helper) = storage_hash_helper.as_deref() { + let helper_function = self.storage_hash2_function(contract.span, helper); + deployment_functions.push(helper_function.clone()); + runtime_functions.push(helper_function); + } + if mapping_value_helper_used { + let helper_function = + self.storage_mapping_value_function(contract.span, STORAGE_MAPPING_VALUE_HELPER); + deployment_functions.push(helper_function.clone()); + runtime_functions.push(helper_function); + } + + let deployer_name = format!("{}Deploy", contract.name); + let runtime_name = contract.name.clone(); + let mut deploy_stmts = Vec::new(); + if let Some((main, span)) = deployment_main { + if let Some(call) = entry_call(&deployment_functions, main, span) { + deploy_stmts.push(call); + } else { + self.push( + span, + EmitDiagnosticKind::UnsupportedDispatchEntry { + signature: "constructor".to_owned(), + reason: "specialized deployment entry function is missing".to_owned(), + }, + ); + } + } else { + self.push( + contract.span, + EmitDiagnosticKind::UnsupportedDispatchEntry { + signature: "constructor".to_owned(), + reason: "missing compiler-generated deployment entry".to_owned(), + }, + ); + } + + let mut runtime_stmts = Vec::new(); + if let Some((main, span)) = runtime_main { + if let Some(call) = entry_call(&runtime_functions, main, span) { + runtime_stmts.push(call); + } else { + self.push( + span, + EmitDiagnosticKind::DispatcherDeferred { + contract: contract.name.clone(), + }, + ); + } + } else { + self.push( + contract.span, + EmitDiagnosticKind::DispatcherDeferred { + contract: contract.name.clone(), + }, + ); + } + + Object { + span: contract.span, + name: deployer_name.into(), + code: CodeBlock { + span: contract.span, + stmts: deploy_stmts, + functions: deployment_functions, + }, + inners: vec![Object { + span: contract.span, + name: runtime_name.into(), + code: CodeBlock { + span: contract.span, + stmts: runtime_stmts, + functions: runtime_functions, + }, + inners: Vec::new(), + }], + } + } +} + +fn entry_call<'db>(functions: &[Function<'db>], name: &str, span: Span<'db>) -> Option> { + let ret = functions + .iter() + .find(|function| function.name.as_str() == name) + .map(|function| function.ret.clone())?; + Some(Stmt { + span, + kind: StmtKind::Expr(Expr { + span, + // Entry return values are ignored by the EVM object code, but the + // call expression must retain the callee's Hull type. + ty: ret, + kind: ExprKind::Call { + callee: name.into(), + args: Vec::new(), + }, + }), + }) +} diff --git a/crates/hull/src/emit/diagnostics.rs b/crates/hull/src/emit/diagnostics.rs new file mode 100644 index 00000000..95eee951 --- /dev/null +++ b/crates/hull/src/emit/diagnostics.rs @@ -0,0 +1,175 @@ +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EmitOptions { + pub emit_dispatcher_comments: bool, +} + +impl Default for EmitOptions { + fn default() -> Self { + Self { + emit_dispatcher_comments: true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmitOutput<'db> { + pub program: Program<'db>, + pub diagnostics: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EmitDiagnostic<'db> { + pub span: Span<'db>, + pub kind: EmitDiagnosticKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmitDiagnosticKind { + UnsupportedType { ty: String }, + UnsupportedLiteral { literal: String }, + UnsupportedMonoConstruct { construct: String }, + MissingAdtLayout { adt: String }, + MissingConstructor { constructor: String, ty: String }, + NonExhaustiveMatch, + MultiScrutineeMatch { count: usize }, + EmptyMatch, + DispatcherDeferred { contract: String }, + UnsupportedDispatchEntry { signature: String, reason: String }, +} + +impl<'db> EmitDiagnostic<'db> { + pub fn lower(&self, db: &'db dyn HirDb) -> Diagnostic { + let mut diagnostic = Diagnostic::error(self.kind.to_string()) + .with_code(self.kind.code()) + .with_primary_label(db, self.span, Some(self.kind.primary_label())); + for note in self.kind.notes() { + diagnostic = diagnostic.with_note(note); + } + diagnostic + } +} + +impl EmitDiagnosticKind { + pub fn code(&self) -> &'static str { + match self { + Self::UnsupportedType { .. } => DiagnosticCode::EMIT_UNSUPPORTED_TYPE, + Self::UnsupportedLiteral { .. } => DiagnosticCode::EMIT_UNSUPPORTED_LITERAL, + Self::UnsupportedMonoConstruct { .. } => { + DiagnosticCode::EMIT_UNSUPPORTED_MONO_CONSTRUCT + } + Self::MissingAdtLayout { .. } => DiagnosticCode::EMIT_MISSING_ADT_LAYOUT, + Self::MissingConstructor { .. } => DiagnosticCode::EMIT_MISSING_CONSTRUCTOR, + Self::NonExhaustiveMatch => DiagnosticCode::EMIT_NON_EXHAUSTIVE_MATCH, + Self::MultiScrutineeMatch { .. } => DiagnosticCode::EMIT_MULTI_SCRUTINEE_MATCH, + Self::EmptyMatch => DiagnosticCode::EMIT_EMPTY_MATCH, + Self::DispatcherDeferred { .. } => DiagnosticCode::EMIT_DISPATCHER_DEFERRED, + Self::UnsupportedDispatchEntry { .. } => { + DiagnosticCode::EMIT_UNSUPPORTED_DISPATCH_ENTRY + } + } + } + + fn primary_label(&self) -> &'static str { + match self { + Self::UnsupportedType { .. } => "unsupported type", + Self::UnsupportedLiteral { .. } => "unsupported literal", + Self::UnsupportedMonoConstruct { .. } => "unsupported construct", + Self::MissingAdtLayout { .. } => "missing ADT layout", + Self::MissingConstructor { .. } => "missing constructor layout", + Self::NonExhaustiveMatch => "match is not exhaustive", + Self::MultiScrutineeMatch { .. } => "multi-scrutinee match", + Self::EmptyMatch => "empty match", + Self::DispatcherDeferred { .. } => "dispatcher cannot be emitted", + Self::UnsupportedDispatchEntry { .. } => "unsupported dispatcher entry", + } + } + + fn notes(&self) -> Vec { + match self { + Self::NonExhaustiveMatch => vec![ + "missing case: _".to_owned(), + "help: add a default or catch-all arm that covers the remaining values".to_owned(), + ], + _ => Vec::new(), + } + } +} + +impl fmt::Display for EmitDiagnosticKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedType { ty } => write!(f, "cannot lower type `{ty}` to Hull"), + Self::UnsupportedLiteral { literal } => { + write!(f, "cannot lower literal `{literal}` to Hull") + } + Self::UnsupportedMonoConstruct { construct } => { + write!(f, "cannot lower {construct} to Hull") + } + Self::MissingAdtLayout { adt } => write!(f, "missing Hull layout for ADT `{adt}`"), + Self::MissingConstructor { constructor, ty } => { + write!( + f, + "missing Hull layout for constructor `{constructor}` of `{ty}`" + ) + } + Self::NonExhaustiveMatch => write!(f, "non-exhaustive pattern match"), + Self::MultiScrutineeMatch { count } => { + write!( + f, + "match with {count} scrutinees is not supported by Hull lowering" + ) + } + Self::EmptyMatch => write!(f, "match has no arms"), + Self::DispatcherDeferred { contract } => { + write!( + f, + "dispatcher generation was deferred for contract `{contract}`" + ) + } + Self::UnsupportedDispatchEntry { signature, reason } => { + write!(f, "cannot emit dispatcher entry `{signature}`: {reason}") + } + } + } +} + +pub(super) fn prune_emit_diagnostics<'db>( + db: &'db dyn hir_ty::Db, + diagnostics: &mut Vec>, +) { + let unsupported_literals = diagnostics + .iter() + .filter_map(|diagnostic| match diagnostic.kind { + EmitDiagnosticKind::UnsupportedLiteral { .. } => Some(diagnostic.span), + _ => None, + }) + .collect::>(); + if unsupported_literals.is_empty() { + return; + } + + diagnostics.retain(|diagnostic| { + if matches!( + diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { .. } + | EmitDiagnosticKind::UnsupportedDispatchEntry { .. } + ) { + !unsupported_literals + .iter() + .any(|literal| span_contains(db, diagnostic.span, *literal)) + } else { + true + } + }); +} + +fn span_contains<'db>(db: &'db dyn HirDb, outer: Span<'db>, inner: Span<'db>) -> bool { + if outer.anchor() == inner.anchor() { + return outer.begin() <= inner.begin() && inner.end() <= outer.end(); + } + let outer = outer.resolve_to_absolute(db); + let inner = inner.resolve_to_absolute(db); + outer.file() == inner.file() && outer.start() <= inner.start() && inner.end() <= outer.end() +} diff --git a/crates/hull/src/emit/emitter.rs b/crates/hull/src/emit/emitter.rs new file mode 100644 index 00000000..5d9c2074 --- /dev/null +++ b/crates/hull/src/emit/emitter.rs @@ -0,0 +1,1214 @@ +use super::*; + +pub fn emit_module<'db>( + db: &'db dyn hir_ty::Db, + module: &MonoModule<'db>, + options: EmitOptions, +) -> EmitOutput<'db> { + Emitter::new(db, module, options).emit(module) +} + +impl<'db> Emitter<'db> { + fn new(db: &'db dyn hir_ty::Db, module: &MonoModule<'db>, options: EmitOptions) -> Self { + let hir_module = parse_file_to_hir(db, module.module.file(db)).module(db); + let if_stmt_spans = module + .frontend_desugar + .bodies + .iter() + .flat_map(|body| &body.transforms) + .filter_map(|transform| match transform { + FrontendTransform::IfStmtToMatch { origin, .. } => Some(origin.span), + _ => None, + }) + .collect(); + Self { + db, + module: hir_module, + _options: options, + diagnostics: Vec::new(), + scopes: ScopeStack::new_root(BTreeMap::new()), + function_names: BTreeSet::new(), + layout_stack: Vec::new(), + if_stmt_spans, + predeclared_lets: Vec::new(), + fresh: 0, + } + } + + fn emit(mut self, module: &MonoModule<'db>) -> EmitOutput<'db> { + let span = self.module.span(self.db); + let mut functions = BTreeMap::>::new(); + let mut contracts = Vec::new(); + self.function_names = module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => Some(function.name.clone()), + _ => None, + }) + .collect(); + for item in &module.items { + match item { + MonoItem::Function(function) => { + let function = self.emit_function(function); + functions.insert(function.name.as_str().to_owned(), function); + } + MonoItem::Contract(contract) => contracts.push(contract.clone()), + MonoItem::Adt(_) => {} + } + } + + let program = if contracts.is_empty() { + Program { + span, + entry_points: module + .entry_points + .iter() + .cloned() + .map(Into::into) + .collect(), + functions: functions.into_values().collect(), + objects: Vec::new(), + } + } else { + let all_functions = functions.values().cloned().collect::>(); + let objects = contracts + .iter() + .map(|contract| self.emit_contract(contract, &all_functions)) + .collect(); + Program { + span, + entry_points: Vec::new(), + functions: Vec::new(), + objects, + } + }; + + prune_emit_diagnostics(self.db, &mut self.diagnostics); + EmitOutput { + program, + diagnostics: self.diagnostics, + } + } + + fn emit_function(&mut self, function: &MonoFunction<'db>) -> Function<'db> { + self.with_scope(|this| { + let args = function + .params + .iter() + .filter_map(|param| { + if param.mode.is_comptime() { + this.push( + param.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: format!("comptime parameter `{}`", param.name), + }, + ); + return None; + } + let ty = this.hull_ty(param.ty.ty(), param.span); + Some(Arg { + span: param.span, + name: param.name.clone().into(), + ty, + }) + }) + .collect::>(); + let ret = this.hull_ty(function.ret.ty(), function.span); + let body = this.emit_stmts(&function.body); + Function { + span: function.span, + name: function.name.clone().into(), + args, + ret, + body, + } + }) + } + + pub(super) fn emit_stmts(&mut self, stmts: &[MonoStmt<'db>]) -> Vec> { + stmts.iter().flat_map(|stmt| self.emit_stmt(stmt)).collect() + } + + fn emit_stmt(&mut self, stmt: &MonoStmt<'db>) -> Vec> { + match &stmt.kind { + MonoStmtKind::Let { id, init, .. } => { + let declared = self.declared_let_ty(stmt); + if let Some(predeclared) = self + .predeclared_lets + .iter() + .find(|predeclared| predeclared.span == stmt.span) + .cloned() + { + // The declaration was hoisted ahead of an `if`. Evaluate the + // initializer before exposing the source name, then assign the + // unique backend local so an outer local or storage field with + // the same spelling remains visible to the initializer. + let rhs = init.as_ref().map(|init| self.emit_expr(init)); + let local = Expr::var(id.span, predeclared.backend_name, predeclared.ty); + self.bind_expr(id.name.clone(), local.clone()); + return rhs + .map(|rhs| { + vec![Stmt { + span: stmt.span, + kind: StmtKind::Assign { lhs: local, rhs }, + }] + }) + .unwrap_or_default(); + } + let mut out = Vec::new(); + if let Some(init) = init { + // The initializer is resolved in the pre-binder scope. Materialize it + // before declaring the source name so downstream name-based lowering + // cannot capture a same-named outer local or storage field. + let rhs = self.emit_expr(init); + let captured_init = expr_reads_var(&rhs, &id.name).then(|| { + let temp = self.fresh_temp("let_init"); + out.push(Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: temp.clone().into(), + ty: declared.clone(), + }, + }); + out.push(Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, temp.clone(), declared.clone()), + rhs: rhs.clone(), + }, + }); + temp + }); + out.push(Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: id.name.clone().into(), + ty: declared.clone(), + }, + }); + out.push(Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, id.name.clone(), declared.clone()), + rhs: captured_init + .map(|temp| Expr::var(stmt.span, temp, declared.clone())) + .unwrap_or(rhs), + }, + }); + } else { + out.push(Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: id.name.clone().into(), + ty: declared.clone(), + }, + }); + } + self.bind_expr( + id.name.clone(), + Expr::var(id.span, id.name.clone(), declared.clone()), + ); + out + } + MonoStmtKind::Return(expr) => { + let expr = expr + .as_ref() + .map(|expr| self.emit_expr(expr)) + .unwrap_or_else(|| Expr::unit(stmt.span)); + vec![Stmt { + span: stmt.span, + kind: StmtKind::Return(expr), + }] + } + MonoStmtKind::Expr(expr) => vec![Stmt { + span: stmt.span, + kind: StmtKind::Expr(self.emit_expr(expr)), + }], + MonoStmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs, + } => vec![Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: self.emit_expr(lhs), + rhs: self.emit_expr(rhs), + }, + }], + MonoStmtKind::Assign { + op: AssignOp::Add, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "add", rhs), + MonoStmtKind::Assign { + op: AssignOp::Sub, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "sub", rhs), + MonoStmtKind::Assign { + op: AssignOp::BitXor, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "xor", rhs), + MonoStmtKind::Assign { + op: AssignOp::BitAnd, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "and", rhs), + MonoStmtKind::Assign { + op: AssignOp::BitOr, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "or", rhs), + MonoStmtKind::Assign { + op: AssignOp::Mod, + lhs, + rhs, + } => self.emit_assign_op(stmt.span, lhs, "mod", rhs), + MonoStmtKind::Match { scrutinees, arms } => { + if self.if_stmt_spans.contains(&stmt.span) + && let ([cond], [then_arm, else_arm]) = (scrutinees.as_slice(), arms.as_slice()) + { + return self.emit_if_stmt( + stmt.span, + cond, + &then_arm.body, + Some(&else_arm.body), + ); + } + self.emit_match(stmt.span, scrutinees, arms) + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => self.emit_if_stmt(stmt.span, cond, then_body, else_body.as_deref()), + MonoStmtKind::Block(body) => vec![Stmt { + span: stmt.span, + kind: StmtKind::Block(self.with_scope(|this| this.emit_stmts(body))), + }], + MonoStmtKind::Assembly(body) => vec![Stmt { + span: stmt.span, + kind: StmtKind::Assembly(body.clone()), + }], + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + // HIR deliberately gives `for` no lexical scope: a let in the + // initializer remains visible in the condition, post/body, and + // after the loop. Hoist the initializer to preserve that model + // in Hull and both backends. + let mut out = self.emit_stmts(init); + let loop_stmt = Stmt { + span: stmt.span, + kind: StmtKind::For { + init: Vec::new(), + cond: self.emit_expr(cond), + post: self.with_scope(|this| this.emit_stmts(post)), + body: self.with_scope(|this| this.emit_stmts(body)), + }, + }; + out.push(loop_stmt); + out + } + MonoStmtKind::Break => vec![Stmt { + span: stmt.span, + kind: StmtKind::Break, + }], + MonoStmtKind::Continue => vec![Stmt { + span: stmt.span, + kind: StmtKind::Continue, + }], + MonoStmtKind::Error => vec![Stmt { + span: stmt.span, + kind: StmtKind::Revert("error statement".to_owned()), + }], + } + } + + fn emit_assign_op( + &mut self, + span: Span<'db>, + lhs: &MonoExpr<'db>, + callee: &str, + rhs: &MonoExpr<'db>, + ) -> Vec> { + let lhs_expr = self.emit_expr(lhs); + let rhs_expr = self.emit_expr(rhs); + let call = Expr { + span, + ty: lhs_expr.ty.clone(), + kind: ExprKind::Call { + callee: callee.to_owned().into(), + args: vec![lhs_expr.clone(), rhs_expr], + }, + }; + vec![Stmt { + span, + kind: StmtKind::Assign { + lhs: lhs_expr, + rhs: call, + }, + }] + } + + fn declared_let_ty(&mut self, stmt: &MonoStmt<'db>) -> Ty<'db> { + let MonoStmtKind::Let { id, ty, init, .. } = &stmt.kind else { + unreachable!("declared_let_ty requires a let statement"); + }; + match ty { + Some(ty) => self.hull_ty(ty.ty(), stmt.span), + None if init.is_none() && sem_ty_needs_untyped_word_default(self.db, id.ty.ty()) => { + Ty::word(stmt.span) + } + None => self.hull_ty(id.ty.ty(), stmt.span), + } + } + + fn emit_if_stmt( + &mut self, + span: Span<'db>, + cond: &MonoExpr<'db>, + then_body: &[MonoStmt<'db>], + else_body: Option<&[MonoStmt<'db>]>, + ) -> Vec> { + let target = self.hull_ty(cond.ty.ty(), cond.span); + let scrutinee = self.emit_expr(cond); + let mut leaking_lets = Vec::new(); + collect_leaking_let_stmts(then_body, &mut leaking_lets); + if let Some(else_body) = else_body { + collect_leaking_let_stmts(else_body, &mut leaking_lets); + } + + let mut out = Vec::new(); + for let_stmt in leaking_lets { + if self + .predeclared_lets + .iter() + .any(|predeclared| predeclared.span == let_stmt.span) + { + continue; + } + let ty = self.declared_let_ty(let_stmt); + let backend_name = self.fresh_temp("if_local"); + self.predeclared_lets.push(PredeclaredLet { + span: let_stmt.span, + backend_name: backend_name.clone(), + ty: ty.clone(), + }); + out.push(Stmt { + span: let_stmt.span, + kind: StmtKind::Let { + name: backend_name.into(), + ty, + }, + }); + } + + // `if` is not a lexical scope in the source language. Emitting the + // branches in source resolution order keeps then-bindings visible to + // the else list and leaves both lists' final bindings visible after it. + let then_stmts = self.emit_stmts(then_body); + let else_stmts = else_body + .map(|body| self.emit_stmts(body)) + .unwrap_or_default(); + out.push(Stmt { + span, + kind: StmtKind::Match { + target, + scrutinee, + alts: vec![ + Alt { + span, + pat: Pat { + span, + kind: PatKind::Con(Con::Inr), + }, + binder: self.fresh_alt().into(), + body: then_stmts, + }, + Alt { + span, + pat: Pat { + span, + kind: PatKind::Con(Con::Inl), + }, + binder: self.fresh_alt().into(), + body: else_stmts, + }, + ], + }, + }); + out + } + + pub(super) fn emit_expr(&mut self, expr: &MonoExpr<'db>) -> Expr<'db> { + if let MonoExprKind::Var(id) = &expr.kind { + if let Some(expr) = self.lookup_expr(&id.name) { + return expr; + } + let ty = self.hull_ty(expr.ty.ty(), expr.span); + return Expr { + span: expr.span, + ty, + kind: ExprKind::Var(id.name.clone().into()), + }; + } + let ty = self.hull_ty(expr.ty.ty(), expr.span); + match &expr.kind { + MonoExprKind::Var(_) => unreachable!("variable expressions return above"), + MonoExprKind::Lit(lit) => self.emit_lit(expr.span, lit), + MonoExprKind::Tuple(elems) => { + let elems = elems + .iter() + .map(|elem| self.emit_expr(elem)) + .collect::>(); + product_expr(expr.span, ty, elems) + } + MonoExprKind::Call { + callee, + args, + origin, + } => Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: call_name(origin, &callee.name).into(), + args: args.iter().map(|arg| self.emit_expr(arg)).collect(), + }, + }, + MonoExprKind::Con { ctor, args } => self.emit_constructor(expr, ctor, args), + MonoExprKind::BinOp { lhs, op, rhs } => self.emit_bin_op(expr.span, ty, lhs, *op, rhs), + MonoExprKind::UnaryOp { op, expr: inner } => { + self.emit_unary_op(expr.span, ty, *op, inner) + } + MonoExprKind::StorageIndex { .. } => Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: STORAGE_INDEX_READ.into(), + args: vec![self.emit_storage_slot_expr(expr)], + }, + }, + MonoExprKind::Conversion { + expr: inner, + kind: + ConversionKind::Identity + | ConversionKind::ValueTypeWrap + | ConversionKind::ValueTypeUnwrap, + .. + } => { + let mut inner = self.emit_expr(inner); + assert!( + same_hull_ty_shape(&inner.ty, &ty), + "checked conversion changed the structural Hull layout" + ); + inner.span = expr.span; + inner.ty = ty; + inner + } + MonoExprKind::Match { scrutinee, arms } => { + self.emit_match_expr(expr, &ty, scrutinee, arms) + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => Expr { + span: expr.span, + ty: ty.clone(), + kind: ExprKind::If { + target: ty, + cond: Box::new(self.emit_expr(cond)), + then_expr: Box::new(self.emit_expr(then_expr)), + else_expr: Box::new(self.emit_expr(else_expr)), + }, + }, + MonoExprKind::ClosureDispatch { callee, args } => { + if let Some(callee_name) = self.closure_callee_name(callee) { + Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: callee_name.into(), + args: args.iter().map(|arg| self.emit_expr(arg)).collect(), + }, + } + } else { + self.push( + expr.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: mono_expr_name(&expr.kind).to_owned(), + }, + ); + Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + } + } + } + MonoExprKind::Field { base, field } => { + if let Ok(index) = field.parse::() { + let fields = sem_product_fields(self.db, base.ty.ty()) + .into_iter() + .map(|field_ty| self.hull_ty(field_ty, base.span)) + .collect::>(); + let base = self.emit_expr(base); + if let Some(field_expr) = product_field_exprs(base, &fields).get(index).cloned() + { + field_expr + } else { + self.push( + expr.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: mono_expr_name(&expr.kind).to_owned(), + }, + ); + Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + } + } + } else { + self.push( + expr.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: mono_expr_name(&expr.kind).to_owned(), + }, + ); + Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + } + } + } + MonoExprKind::Index { .. } + | MonoExprKind::Proxy(_) + | MonoExprKind::Lambda { .. } + | MonoExprKind::Error => { + self.push( + expr.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: mono_expr_name(&expr.kind).to_owned(), + }, + ); + Expr { + span: expr.span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + } + } + } + } + + fn emit_match_expr( + &mut self, + expr: &MonoExpr<'db>, + ty: &Ty<'db>, + scrutinee: &MonoExpr<'db>, + arms: &[MonoExprArm<'db>], + ) -> Expr<'db> { + // Mono expression matches currently reach Hull as the pre-typecheck + // lowering for `if` expressions. General expression-match lowering + // needs a language-spec decision about branch result sequencing and + // exhaustiveness before this backend should grow a broader lowering. + let Some((then_expr, else_expr)) = bool_match_expr_arms(self.db, arms) else { + self.push( + expr.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: "expression match".to_owned(), + }, + ); + return Expr { + span: expr.span, + ty: ty.clone(), + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + }; + }; + Expr { + span: expr.span, + ty: ty.clone(), + kind: ExprKind::If { + target: ty.clone(), + cond: Box::new(self.emit_expr(scrutinee)), + then_expr: Box::new(self.emit_expr(then_expr)), + else_expr: Box::new(self.emit_expr(else_expr)), + }, + } + } + + fn closure_callee_name(&self, callee: &MonoExpr<'db>) -> Option { + let name = match &callee.kind { + MonoExprKind::Var(id) => &id.name, + MonoExprKind::Lambda { name, .. } => name, + MonoExprKind::Conversion { + expr, + kind: ConversionKind::Identity, + .. + } => return self.closure_callee_name(expr), + _ => return None, + }; + self.function_names.contains(name).then(|| name.clone()) + } + + fn emit_lit(&mut self, span: Span<'db>, lit: &LitKind) -> Expr<'db> { + match lit { + LitKind::Number(value) | LitKind::Hex(value) => Expr::word(span, wrap_lit_text(value)), + LitKind::String(value) => { + self.push( + span, + EmitDiagnosticKind::UnsupportedLiteral { + literal: value.clone(), + }, + ); + Expr::word(span, "0") + } + LitKind::Error => Expr::word(span, "0"), + } + } + + fn emit_storage_slot_expr(&mut self, expr: &MonoExpr<'db>) -> Expr<'db> { + match &expr.kind { + MonoExprKind::StorageIndex { base, index } => Expr { + span: expr.span, + ty: Ty::word(expr.span), + kind: ExprKind::Call { + callee: STORAGE_INDEX_SLOT.into(), + args: vec![self.emit_storage_slot_expr(base), self.emit_expr(index)], + }, + }, + _ => self.emit_expr(expr), + } + } + + fn emit_constructor( + &mut self, + expr: &MonoExpr<'db>, + ctor: &MonoId<'db>, + args: &[MonoExpr<'db>], + ) -> Expr<'db> { + let target = if sem_ty_needs_untyped_word_default(self.db, expr.ty.ty()) { + Ty::word(expr.span) + } else { + self.hull_ty(expr.ty.ty(), expr.span) + }; + let ctor_name = ctor.name.as_str(); + match ctor.builtin_ctor(self.db) { + Some(MonoBuiltinCtor::Unit) => return Expr::unit(expr.span), + Some(MonoBuiltinCtor::Pair) => { + let args = args.iter().map(|arg| self.emit_expr(arg)).collect(); + return product_expr(expr.span, target, args); + } + Some(MonoBuiltinCtor::True) => { + let payload = Expr::unit(expr.span); + return Expr { + span: expr.span, + ty: target.clone(), + kind: ExprKind::Inr { + target, + value: Box::new(payload), + }, + }; + } + Some(MonoBuiltinCtor::False) => { + let payload = Expr::unit(expr.span); + return Expr { + span: expr.span, + ty: target.clone(), + kind: ExprKind::Inl { + target, + value: Box::new(payload), + }, + }; + } + Some(MonoBuiltinCtor::Inl | MonoBuiltinCtor::Inr) if args.len() == 1 => { + let value = self.emit_expr(&args[0]); + return Expr { + span: expr.span, + ty: target.clone(), + kind: if ctor.is_builtin_ctor(self.db, MonoBuiltinCtor::Inl) { + ExprKind::Inl { + target, + value: Box::new(value), + } + } else { + ExprKind::Inr { + target, + value: Box::new(value), + } + }, + }; + } + _ => {} + } + match ctor_name { + "uint256" | "uint" | "bytes32" | "address" if args.len() == 1 => { + let mut value = self.emit_expr(&args[0]); + value.ty = if sem_ty_needs_untyped_word_default(self.db, expr.ty.ty()) { + Ty::word(expr.span) + } else { + target + }; + return value; + } + _ => {} + } + + let Some(layout) = self.adt_layout_for_sem_ty(expr.ty.ty(), expr.span) else { + self.push( + expr.span, + EmitDiagnosticKind::MissingAdtLayout { + adt: expr.ty.ty().display(self.db), + }, + ); + return Expr { + span: expr.span, + ty: target, + kind: ExprKind::Call { + callee: ctor_name.into(), + args: args.iter().map(|arg| self.emit_expr(arg)).collect(), + }, + }; + }; + let Some(index) = constructor_index(&layout, ctor_name) else { + self.push( + expr.span, + EmitDiagnosticKind::MissingConstructor { + constructor: ctor_name.to_owned(), + ty: layout.name, + }, + ); + return Expr { + span: expr.span, + ty: target, + kind: ExprKind::Call { + callee: ctor_name.into(), + args: args.iter().map(|arg| self.emit_expr(arg)).collect(), + }, + }; + }; + let payload_ty = layout.ctors[index].payload.clone(); + let payload_args = args + .iter() + .map(|arg| self.emit_expr(arg)) + .collect::>(); + let payload = product_expr(expr.span, payload_ty, payload_args); + encode_constructor(expr.span, layout.target, index, layout.ctors.len(), payload) + } + + fn emit_bin_op( + &mut self, + span: Span<'db>, + ty: Ty<'db>, + lhs: &MonoExpr<'db>, + op: BinOp, + rhs: &MonoExpr<'db>, + ) -> Expr<'db> { + match op { + BinOp::NotEq => { + let eq = Expr { + span, + ty: ty.clone(), + kind: ExprKind::Call { + callee: "primEqWord".into(), + args: vec![self.emit_expr(lhs), self.emit_expr(rhs)], + }, + }; + return Expr { + span, + ty: ty.clone(), + kind: ExprKind::Call { + callee: "iszero".into(), + args: vec![eq], + }, + }; + } + BinOp::LtEq | BinOp::GtEq => { + let callee = if matches!(op, BinOp::LtEq) { + "gt" + } else { + "lt" + }; + let cmp = Expr { + span, + ty: ty.clone(), + kind: ExprKind::Call { + callee: callee.into(), + args: vec![self.emit_expr(lhs), self.emit_expr(rhs)], + }, + }; + return Expr { + span, + ty: ty.clone(), + kind: ExprKind::Call { + callee: "iszero".into(), + args: vec![cmp], + }, + }; + } + BinOp::And => { + return Expr { + span, + ty: ty.clone(), + kind: ExprKind::If { + target: ty.clone(), + cond: Box::new(self.emit_expr(lhs)), + then_expr: Box::new(self.emit_expr(rhs)), + else_expr: Box::new(bool_expr(span, ty, false)), + }, + }; + } + BinOp::Or => { + return Expr { + span, + ty: ty.clone(), + kind: ExprKind::If { + target: ty.clone(), + cond: Box::new(self.emit_expr(lhs)), + then_expr: Box::new(bool_expr(span, ty.clone(), true)), + else_expr: Box::new(self.emit_expr(rhs)), + }, + }; + } + _ => {} + } + let Some(callee) = bin_op_name(op) else { + self.push( + span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: format!("binary operator {op:?}"), + }, + ); + return Expr { + span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + }; + }; + Expr { + span, + ty, + kind: ExprKind::Call { + callee: callee.into(), + args: if matches!(op, BinOp::Shl | BinOp::Shr) { + vec![self.emit_expr(rhs), self.emit_expr(lhs)] + } else { + vec![self.emit_expr(lhs), self.emit_expr(rhs)] + }, + }, + } + } + + fn emit_unary_op( + &mut self, + span: Span<'db>, + ty: Ty<'db>, + op: UnOp, + expr: &MonoExpr<'db>, + ) -> Expr<'db> { + match op { + UnOp::Not => { + let false_expr = Expr { + span, + ty: ty.clone(), + kind: ExprKind::Inl { + target: ty.clone(), + value: Box::new(Expr::unit(span)), + }, + }; + let true_expr = Expr { + span, + ty: ty.clone(), + kind: ExprKind::Inr { + target: ty.clone(), + value: Box::new(Expr::unit(span)), + }, + }; + Expr { + span, + ty: ty.clone(), + kind: ExprKind::If { + target: ty, + cond: Box::new(self.emit_expr(expr)), + then_expr: Box::new(false_expr), + else_expr: Box::new(true_expr), + }, + } + } + UnOp::Error => { + self.push( + span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: "unary error".to_owned(), + }, + ); + Expr { + span, + ty, + kind: ExprKind::Call { + callee: "unsupported".into(), + args: Vec::new(), + }, + } + } + } + } + + pub(super) fn fresh_alt(&mut self) -> String { + let name = format!("$alt{}", self.fresh); + self.fresh += 1; + name + } + + pub(super) fn fresh_temp(&mut self, purpose: &str) -> String { + let name = format!("${purpose}{}", self.fresh); + self.fresh += 1; + name + } + + pub(super) fn bind_expr(&mut self, name: String, expr: Expr<'db>) { + self.scopes.last_mut().insert(name, expr); + } + + fn lookup_expr(&self, name: &str) -> Option> { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + } + + pub(super) fn with_scope(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + self.scopes.push(BTreeMap::new()); + let out = f(self); + let _ = self.scopes.pop(); + out + } + + pub(super) fn push(&mut self, span: Span<'db>, kind: EmitDiagnosticKind) { + self.diagnostics.push(EmitDiagnostic { span, kind }); + } +} + +pub(super) fn expr_reads_var(expr: &Expr<'_>, expected: &str) -> bool { + match &expr.kind { + ExprKind::Var(name) => name.as_str() == expected, + ExprKind::Pair(lhs, rhs) => expr_reads_var(lhs, expected) || expr_reads_var(rhs, expected), + ExprKind::Fst(expr) + | ExprKind::Snd(expr) + | ExprKind::Inl { value: expr, .. } + | ExprKind::Inr { value: expr, .. } + | ExprKind::InK { value: expr, .. } => expr_reads_var(expr, expected), + ExprKind::Call { args, .. } => args.iter().any(|arg| expr_reads_var(arg, expected)), + ExprKind::If { + cond, + then_expr, + else_expr, + .. + } => { + expr_reads_var(cond, expected) + || expr_reads_var(then_expr, expected) + || expr_reads_var(else_expr, expected) + } + ExprKind::Word(_) | ExprKind::Bool(_) | ExprKind::Unit => false, + } +} + +fn collect_leaking_let_stmts<'a, 'db>( + stmts: &'a [MonoStmt<'db>], + out: &mut Vec<&'a MonoStmt<'db>>, +) { + for stmt in stmts { + match &stmt.kind { + MonoStmtKind::Let { .. } => out.push(stmt), + MonoStmtKind::If { + then_body, + else_body, + .. + } => { + collect_leaking_let_stmts(then_body, out); + if let Some(else_body) = else_body { + collect_leaking_let_stmts(else_body, out); + } + } + MonoStmtKind::For { + init, post, body, .. + } => { + collect_leaking_let_stmts(init, out); + collect_leaking_let_stmts(post, out); + collect_leaking_let_stmts(body, out); + } + // Explicit blocks and match alternatives retain lexical scopes. + MonoStmtKind::Match { .. } + | MonoStmtKind::Block(_) + | MonoStmtKind::Return(_) + | MonoStmtKind::Expr(_) + | MonoStmtKind::Assign { .. } + | MonoStmtKind::Assembly(_) + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Error => {} + } + } +} + +fn call_name(origin: &MonoCallOrigin<'_>, name: &str) -> String { + match origin { + MonoCallOrigin::Builtin(intrinsic) => intrinsic_name(*intrinsic).to_owned(), + MonoCallOrigin::Source(_) | MonoCallOrigin::ByName => name.to_owned(), + } +} + +fn intrinsic_name(intrinsic: MonoIntrinsic) -> &'static str { + match intrinsic { + MonoIntrinsic::PrimAddWord => "primAddWord", + MonoIntrinsic::PrimEqWord => "primEqWord", + MonoIntrinsic::SubWord => "subWord", + MonoIntrinsic::MulWord => "mulWord", + MonoIntrinsic::GtWord => "gtWord", + MonoIntrinsic::BxorWord => "bxorWord", + MonoIntrinsic::BandWord => "bandWord", + MonoIntrinsic::BorWord => "borWord", + MonoIntrinsic::WordToInteger => "wordToInteger", + MonoIntrinsic::WordFromInteger => "wordFromInteger", + MonoIntrinsic::IntegerAdd => "integerAdd", + MonoIntrinsic::IntegerSub => "integerSub", + MonoIntrinsic::IntegerMul => "integerMul", + MonoIntrinsic::IntegerLt => "integerLt", + MonoIntrinsic::IntegerEq => "integerEq", + MonoIntrinsic::ConcatLit => "concatLit", + MonoIntrinsic::StrlenLit => "strlenLit", + MonoIntrinsic::KeccakLit => "keccakLit", + } +} + +fn bin_op_name(op: BinOp) -> Option<&'static str> { + match op { + BinOp::Add => Some("add"), + BinOp::Sub => Some("sub"), + BinOp::Mul => Some("mul"), + BinOp::Div => Some("div"), + BinOp::Mod => Some("mod"), + BinOp::Pow => Some("exp"), + BinOp::Shl => Some("shl"), + BinOp::Shr => Some("shr"), + BinOp::BitAnd => Some("and"), + BinOp::BitXor => Some("xor"), + BinOp::BitOr => Some("or"), + BinOp::Eq => Some("primEqWord"), + BinOp::Lt => Some("lt"), + BinOp::Gt => Some("gt"), + BinOp::NotEq | BinOp::LtEq | BinOp::GtEq | BinOp::And | BinOp::Or | BinOp::Error => None, + } +} + +fn mono_expr_name(kind: &MonoExprKind<'_>) -> &'static str { + match kind { + MonoExprKind::Field { .. } => "field access", + MonoExprKind::Index { .. } => "index access", + MonoExprKind::StorageIndex { .. } => "storage index access", + MonoExprKind::Match { .. } => "expression match", + MonoExprKind::Proxy(_) => "proxy expression", + MonoExprKind::Lambda { .. } => "lambda expression", + MonoExprKind::ClosureDispatch { .. } => "closure dispatch", + MonoExprKind::Error => "error expression", + _ => "expression", + } +} + +fn same_hull_ty_shape(lhs: &Ty<'_>, rhs: &Ty<'_>) -> bool { + match (&lhs.kind, &rhs.kind) { + (TyKind::Word, TyKind::Word) + | (TyKind::Bool, TyKind::Bool) + | (TyKind::Unit, TyKind::Unit) => true, + (TyKind::Product(lhs_head, lhs_tail), TyKind::Product(rhs_head, rhs_tail)) + | (TyKind::Sum(lhs_head, lhs_tail), TyKind::Sum(rhs_head, rhs_tail)) => { + same_hull_ty_shape(lhs_head, rhs_head) && same_hull_ty_shape(lhs_tail, rhs_tail) + } + ( + TyKind::Named { + name: lhs_name, + inner: lhs_inner, + }, + TyKind::Named { + name: rhs_name, + inner: rhs_inner, + }, + ) => lhs_name == rhs_name && same_hull_ty_shape(lhs_inner, rhs_inner), + (TyKind::NamedRef { name: lhs }, TyKind::NamedRef { name: rhs }) => lhs == rhs, + ( + TyKind::Function { + params: lhs_params, + ret: lhs_ret, + }, + TyKind::Function { + params: rhs_params, + ret: rhs_ret, + }, + ) => { + lhs_params.len() == rhs_params.len() + && lhs_params + .iter() + .zip(rhs_params) + .all(|(lhs, rhs)| same_hull_ty_shape(lhs, rhs)) + && same_hull_ty_shape(lhs_ret, rhs_ret) + } + _ => false, + } +} + +fn bool_match_expr_arms<'a, 'db>( + db: &'db dyn hir_ty::Db, + arms: &'a [MonoExprArm<'db>], +) -> Option<(&'a MonoExpr<'db>, &'a MonoExpr<'db>)> { + let mut then_expr = None; + let mut else_expr = None; + for arm in arms { + match bool_constructor_pat_value(db, &arm.pat)? { + true if then_expr.is_none() => then_expr = Some(&arm.expr), + false if else_expr.is_none() => else_expr = Some(&arm.expr), + _ => return None, + } + } + Some((then_expr?, else_expr?)) +} + +fn bool_constructor_pat_value<'db>(db: &'db dyn hir_ty::Db, pat: &MonoPat<'db>) -> Option { + match &pat.kind { + MonoPatKind::Con { ctor, args } + if args.is_empty() && ctor.is_builtin_ctor(db, MonoBuiltinCtor::True) => + { + Some(true) + } + MonoPatKind::Con { ctor, args } + if args.is_empty() && ctor.is_builtin_ctor(db, MonoBuiltinCtor::False) => + { + Some(false) + } + _ => None, + } +} diff --git a/crates/hull/src/emit/layout.rs b/crates/hull/src/emit/layout.rs new file mode 100644 index 00000000..e108aaf9 --- /dev/null +++ b/crates/hull/src/emit/layout.rs @@ -0,0 +1,422 @@ +use super::*; + +impl<'db> Emitter<'db> { + pub(super) fn hull_ty(&mut self, ty: SemTy<'db>, span: Span<'db>) -> Ty<'db> { + match self.try_hull_ty(ty, span) { + Some(ty) => ty, + None => { + self.push( + span, + EmitDiagnosticKind::UnsupportedType { + ty: ty.display(self.db), + }, + ); + Ty::word(span) + } + } + } + + pub(super) fn try_hull_ty(&mut self, ty: SemTy<'db>, span: Span<'db>) -> Option> { + match ty.kind(self.db) { + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Word), + args, + } if args.is_empty() => Some(Ty::word(span)), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Some(Ty::unit(span)), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Some(bool_sum_ty(span)), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => Some(Ty::product( + span, + self.hull_ty(args[0], span), + self.hull_ty(args[1], span), + )), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Sum), + args, + } if args.len() == 2 => Some(Ty::sum( + span, + self.hull_ty(args[0], span), + self.hull_ty(args[1], span), + )), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => None, + SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } if matches!(user.kind, UserTyCtorKind::ValueType) && args.is_empty() => { + let underlying = value_type_underlying(self.db, user.def).ok()?; + self.try_hull_ty(underlying, span) + } + SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } if matches!(user.kind, UserTyCtorKind::Adt) => { + let layout = self.adt_layout(user.def, args, span)?; + Some(layout.target) + } + SemTyKind::Function { params, ret } => Some(Ty::function( + span, + params + .iter() + .map(|param| self.hull_ty(*param, span)) + .collect(), + self.hull_ty(*ret, span), + )), + SemTyKind::Tuple(elems) => Some(tuple_ty( + span, + elems.iter().map(|elem| self.hull_ty(*elem, span)).collect(), + )), + SemTyKind::Comptime(inner) => self.try_hull_ty(*inner, span), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Integer | BuiltinTyCtor::String), + .. + } + | SemTyKind::Named { .. } + | SemTyKind::BoundVar(_) => None, + SemTyKind::Error | SemTyKind::Unknown => Some(Ty::word(span)), + } + } + + pub(super) fn adt_layout_for_sem_ty( + &mut self, + ty: SemTy<'db>, + span: Span<'db>, + ) -> Option> { + match ty.kind(self.db) { + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Bool), + args, + } if args.is_empty() => Some(AdtLayout { + name: "Bool".to_owned(), + target: bool_sum_ty(span), + ctors: vec![ + CtorLayout { + name: "false".to_owned(), + payload: Ty::unit(span), + fields: Vec::new(), + }, + CtorLayout { + name: "true".to_owned(), + payload: Ty::unit(span), + fields: Vec::new(), + }, + ], + }), + SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } if matches!(user.kind, UserTyCtorKind::Adt) => self.adt_layout(user.def, args, span), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Sum), + args, + } if args.len() == 2 => Some(AdtLayout { + name: "sum".to_owned(), + target: self.hull_ty(ty, span), + ctors: vec![ + CtorLayout { + name: "inl".to_owned(), + payload: self.hull_ty(args[0], span), + fields: vec![args[0]], + }, + CtorLayout { + name: "inr".to_owned(), + payload: self.hull_ty(args[1], span), + fields: vec![args[1]], + }, + ], + }), + _ => None, + } + } + + fn adt_layout( + &mut self, + def: DefId<'db>, + args: &[SemTy<'db>], + span: Span<'db>, + ) -> Option> { + let module = parse_file_to_hir(self.db, def.file(self.db)).module(self.db); + let adt = find_adt(self.db, module, def)?; + let name = def.name(self.db).unwrap_or_else(|| "Adt".to_owned()); + let layout_key = (def, args.to_vec()); + if self.layout_stack.contains(&layout_key) { + return Some(AdtLayout { + name: name.clone(), + target: Ty::named_ref(span, name), + ctors: Vec::new(), + }); + } + + self.layout_stack.push(layout_key); + let Some(plan) = hir_ty::derived_generic_plan(self.db, module, adt) else { + self.layout_stack.pop(); + return None; + }; + let rep = subst_sem_ty(self.db, plan.rep, args); + let inner = self.hull_ty(rep, span); + let target = Ty::named(span, name.clone(), inner); + let ctors = plan + .from_arms + .iter() + .map(|arm| { + let product = subst_sem_ty(self.db, arm.product_rep, args); + Some(CtorLayout { + name: arm.ctor_name.clone(), + payload: self.hull_ty(product, span), + fields: sem_product_fields_exact(self.db, product, arm.field_count as usize)?, + }) + }) + .collect::>>()?; + self.layout_stack.pop(); + Some(AdtLayout { + name, + target, + ctors, + }) + } +} + +pub(super) fn sem_ty_needs_untyped_word_default<'db>( + db: &'db dyn hir_ty::Db, + ty: SemTy<'db>, +) -> bool { + matches!(ty.kind(db), SemTyKind::Error | SemTyKind::Unknown) +} + +pub(super) fn hull_ty_word_slots(ty: &Ty<'_>) -> Option { + match &ty.strip_named().kind { + TyKind::Word | TyKind::Bool | TyKind::NamedRef { .. } | TyKind::Function { .. } => Some(1), + TyKind::Unit => Some(0), + TyKind::Product(lhs, rhs) => Some(hull_ty_word_slots(lhs)? + hull_ty_word_slots(rhs)?), + TyKind::Sum(lhs, rhs) => Some(1 + hull_ty_word_slots(lhs)?.max(hull_ty_word_slots(rhs)?)), + TyKind::Named { inner, .. } => hull_ty_word_slots(inner), + } +} + +pub(super) fn bool_expr<'db>(span: Span<'db>, target: Ty<'db>, value: bool) -> Expr<'db> { + let payload = Expr::unit(span); + let kind = if value { + ExprKind::Inr { + target: target.clone(), + value: Box::new(payload), + } + } else { + ExprKind::Inl { + target: target.clone(), + value: Box::new(payload), + } + }; + Expr { + span, + ty: target, + kind, + } +} + +pub(super) fn sem_product_fields<'db>(db: &'db dyn hir_ty::Db, ty: SemTy<'db>) -> Vec> { + match ty.kind(db) { + SemTyKind::Tuple(elems) => elems + .iter() + .flat_map(|elem| sem_product_fields(db, *elem)) + .collect(), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Vec::new(), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => { + let mut out = vec![args[0]]; + out.extend(sem_product_fields(db, args[1])); + out + } + _ => vec![ty], + } +} + +fn sem_product_fields_exact<'db>( + db: &'db dyn hir_ty::Db, + mut product: SemTy<'db>, + arity: usize, +) -> Option>> { + if arity == 0 { + return match product.kind(db) { + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Some(Vec::new()), + _ => None, + }; + } + let mut fields = Vec::with_capacity(arity); + for _ in 1..arity { + let SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } = product.kind(db) + else { + return None; + }; + if args.len() != 2 { + return None; + } + fields.push(args[0]); + product = args[1]; + } + fields.push(product); + Some(fields) +} + +pub(super) fn product_field_exprs<'db>(base: Expr<'db>, fields: &[Ty<'db>]) -> Vec> { + match fields { + [] => Vec::new(), + [field] => { + let mut expr = base; + expr.ty = field.clone(); + vec![expr] + } + [head, tail @ ..] => { + let lhs = Expr { + span: base.span, + ty: head.clone(), + kind: ExprKind::Fst(Box::new(base.clone())), + }; + let rhs = Expr { + span: base.span, + ty: product_right_ty(&base.ty), + kind: ExprKind::Snd(Box::new(base)), + }; + let mut out = vec![lhs]; + out.extend(product_field_exprs(rhs, tail)); + out + } + } +} + +pub(super) fn product_expr<'db>(span: Span<'db>, ty: Ty<'db>, elems: Vec>) -> Expr<'db> { + product_expr_from_slice(span, ty, &elems) +} + +fn product_expr_from_slice<'db>(span: Span<'db>, ty: Ty<'db>, elems: &[Expr<'db>]) -> Expr<'db> { + match elems { + [] => Expr::unit(span), + [one] => { + let mut one = one.clone(); + one.ty = ty; + one + } + [head, tail @ ..] => { + let tail_ty = product_right_ty(&ty); + Expr { + span, + ty: ty.clone(), + kind: ExprKind::Pair( + Box::new(head.clone()), + Box::new(product_expr_from_slice(span, tail_ty, tail)), + ), + } + } + } +} + +fn tuple_ty<'db>(span: Span<'db>, elems: Vec>) -> Ty<'db> { + tuple_ty_from_slice(span, &elems) +} + +fn tuple_ty_from_slice<'db>(span: Span<'db>, elems: &[Ty<'db>]) -> Ty<'db> { + match elems { + [] => Ty::unit(span), + [one] => one.clone(), + [head, tail @ ..] => Ty::product(span, head.clone(), tuple_ty_from_slice(span, tail)), + } +} + +pub(super) fn bool_sum_ty<'db>(span: Span<'db>) -> Ty<'db> { + Ty::sum(span, Ty::unit(span), Ty::unit(span)) +} + +fn product_right_ty<'db>(ty: &Ty<'db>) -> Ty<'db> { + match &ty.strip_named().kind { + TyKind::Product(_, rhs) => (**rhs).clone(), + _ => Ty::unit(ty.span), + } +} + +pub(super) fn sum_right_ty<'db>(ty: &Ty<'db>) -> Ty<'db> { + match &ty.strip_named().kind { + TyKind::Sum(_, rhs) => (**rhs).clone(), + _ => Ty::unit(ty.span), + } +} + +pub(super) fn find_adt<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module + .items(db) + .iter() + .find_map(|item| find_adt_in_item(db, *item, def)) +} + +fn find_adt_in_item<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + def: DefId<'db>, +) -> Option> { + match item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match item { + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => Some(*adt), + _ => None, + }), + _ => None, + } +} + +pub(super) fn subst_sem_ty<'db>( + db: &'db dyn hir_ty::Db, + ty: SemTy<'db>, + args: &[SemTy<'db>], +) -> SemTy<'db> { + match ty.kind(db) { + SemTyKind::BoundVar(var) => args.get(var.index as usize).copied().unwrap_or(ty), + SemTyKind::Named { ctor, args: inner } => SemTy::named( + db, + *ctor, + inner + .iter() + .map(|arg| subst_sem_ty(db, *arg, args)) + .collect(), + ), + SemTyKind::Function { params, ret } => SemTy::function( + db, + params + .iter() + .map(|param| subst_sem_ty(db, *param, args)) + .collect(), + subst_sem_ty(db, *ret, args), + ), + SemTyKind::Tuple(elems) => SemTy::tuple( + db, + elems + .iter() + .map(|elem| subst_sem_ty(db, *elem, args)) + .collect(), + ), + SemTyKind::Comptime(inner) => SemTy::comptime(db, subst_sem_ty(db, *inner, args)), + SemTyKind::Error | SemTyKind::Unknown => ty, + } +} diff --git a/crates/hull/src/emit/match_compile.rs b/crates/hull/src/emit/match_compile.rs new file mode 100644 index 00000000..833c13e0 --- /dev/null +++ b/crates/hull/src/emit/match_compile.rs @@ -0,0 +1,1324 @@ +use super::*; + +#[derive(Debug, Clone)] +pub(super) struct AdtLayout<'db> { + pub(super) name: String, + pub(super) target: Ty<'db>, + pub(super) ctors: Vec>, +} + +#[derive(Debug, Clone)] +pub(super) struct CtorLayout<'db> { + pub(super) name: String, + pub(super) payload: Ty<'db>, + pub(super) fields: Vec>, +} + +#[derive(Debug, Clone)] +struct Branch<'db> { + binder: String, + body: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct Occurrence(Vec); + +#[derive(Debug, Clone)] +struct MatchColumn<'db> { + occurrence: Occurrence, + ty: SemTy<'db>, + span: Span<'db>, +} + +#[derive(Debug, Clone)] +struct MatchRow<'db> { + span: Span<'db>, + pats: Vec, + bindings: Vec<(String, Occurrence)>, + body: Vec>, +} + +#[derive(Debug, Clone)] +enum MatrixPat { + Wildcard, + Var { name: String }, + Lit { lit: LitKind }, + Con { ctor: String, args: Vec }, + Tuple { elems: Vec }, + ComptimeLabel, + Error, +} + +#[derive(Debug, Clone)] +enum DecisionTree<'db> { + Leaf { + bindings: Vec<(String, Occurrence)>, + body: Vec>, + }, + Fail { + span: Span<'db>, + }, + Product { + occurrence: Occurrence, + fields: Vec>, + subtree: Box>, + }, + Switch { + occurrence: Occurrence, + layout: AdtLayout<'db>, + branches: Vec>, + default: Option>>, + }, + AtomicSwitch { + occurrence: Occurrence, + target: Ty<'db>, + branches: Vec>, + default: Option>>, + }, +} + +#[derive(Debug, Clone)] +struct CtorDecision<'db> { + index: usize, + tree: DecisionTree<'db>, +} + +#[derive(Debug, Clone)] +struct AtomicDecision<'db> { + lit: LitKind, + tree: DecisionTree<'db>, +} + +struct MatchMatrix<'db> { + columns: Vec>, + rows: Vec>, +} + +struct MatrixState<'db> { + test: MatchColumn<'db>, + rest: Vec>, + rows: Vec>, +} + +impl<'db> MatchMatrix<'db> { + fn new(columns: Vec>, rows: Vec>) -> Self { + Self { columns, rows } + } + + fn rows_is_empty(&self) -> bool { + self.rows.is_empty() + } + + fn fail_span(&self, fallback: Span<'db>) -> Span<'db> { + self.columns + .first() + .map(|column| column.span) + .unwrap_or(fallback) + } + + fn columns_is_empty(&self) -> bool { + self.columns.is_empty() + } + + fn first_row_is_var_like(&self) -> bool { + self.rows + .first() + .is_some_and(|row| row.pats.iter().all(MatrixPat::is_var_like)) + } + + fn into_first_leaf(self) -> DecisionTree<'db> { + let row = self.rows.into_iter().next().expect("row exists"); + DecisionTree::Leaf { + bindings: row.bindings, + body: row.body, + } + } + + fn into_var_like_leaf(self) -> DecisionTree<'db> { + let row = self.rows.into_iter().next().expect("row exists"); + let MatchRow { + span: _, + pats, + mut bindings, + body, + } = row; + for (pat, column) in pats.into_iter().zip(self.columns) { + if let MatrixPat::Var { name } = pat { + bindings.push((name, column.occurrence)); + } + } + DecisionTree::Leaf { bindings, body } + } + + fn into_selected_state(mut self) -> MatrixState<'db> { + debug_assert!(!self.columns.is_empty()); + let selected = select_match_column(&self.columns, &self.rows); + move_selected_column_to_front(&mut self.columns, selected); + move_selected_pat_to_front(&mut self.rows, selected); + let test = self.columns.remove(0); + MatrixState { + test, + rest: self.columns, + rows: self.rows, + } + } +} + +impl<'db> MatrixState<'db> { + fn first_col(&self) -> Vec<&MatrixPat> { + self.rows + .iter() + .filter_map(|row| row.pats.first()) + .collect() + } + + fn into_default(self) -> (Vec>, Vec>) { + default_rows(self.test.occurrence, self.rows, self.rest) + } +} + +impl<'db> Emitter<'db> { + pub(super) fn emit_match( + &mut self, + span: Span<'db>, + scrutinees: &[MonoExpr<'db>], + arms: &[MonoArm<'db>], + ) -> Vec> { + if scrutinees.is_empty() { + self.push(span, EmitDiagnosticKind::EmptyMatch); + return vec![Stmt { + span, + kind: StmtKind::Revert("empty match".to_owned()), + }]; + } + if arms.is_empty() { + self.push(span, EmitDiagnosticKind::EmptyMatch); + return vec![Stmt { + span, + kind: StmtKind::Revert("empty match".to_owned()), + }]; + } + + let mut materialized_scrutinees = Vec::new(); + let mut scrutinee_exprs = Vec::with_capacity(scrutinees.len()); + for scrutinee in scrutinees { + let expr = self.emit_expr(scrutinee); + if matches!( + &expr.kind, + ExprKind::Word(_) | ExprKind::Bool(_) | ExprKind::Unit | ExprKind::Var(_) + ) { + scrutinee_exprs.push(expr); + continue; + } + let temp = self.fresh_temp("match_scrutinee"); + let ty = expr.ty.clone(); + materialized_scrutinees.push(Stmt { + span: scrutinee.span, + kind: StmtKind::Let { + name: temp.clone().into(), + ty: ty.clone(), + }, + }); + materialized_scrutinees.push(Stmt { + span: scrutinee.span, + kind: StmtKind::Assign { + lhs: Expr::var(scrutinee.span, temp.clone(), ty.clone()), + rhs: expr, + }, + }); + scrutinee_exprs.push(Expr::var(scrutinee.span, temp, ty)); + } + let columns = scrutinees + .iter() + .enumerate() + .map(|(index, scrutinee)| MatchColumn { + occurrence: Occurrence(vec![index]), + ty: scrutinee.ty.ty(), + span: scrutinee.span, + }) + .collect::>(); + let rows = arms + .iter() + .filter_map(|arm| { + if arm.pats.len() != scrutinees.len() { + self.push( + arm.span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: "match arm arity mismatch".to_owned(), + }, + ); + return None; + } + Some(MatchRow { + span: arm.span, + pats: arm + .pats + .iter() + .map(|pat| matrix_pat(self.db, pat)) + .collect(), + bindings: Vec::new(), + body: arm.body.clone(), + }) + }) + .collect::>(); + if rows.is_empty() { + self.push(span, EmitDiagnosticKind::EmptyMatch); + return vec![Stmt { + span, + kind: StmtKind::Revert("empty match".to_owned()), + }]; + } + + let mut occurrences = columns + .iter() + .zip(scrutinee_exprs) + .map(|(column, expr)| (column.occurrence.clone(), expr)) + .collect::>(); + let tree = self.compile_match_matrix(span, MatchMatrix::new(columns, rows)); + materialized_scrutinees.extend(self.tree_to_body(span, &mut occurrences, &tree)); + materialized_scrutinees + } + + fn compile_match_matrix( + &mut self, + span: Span<'db>, + matrix: MatchMatrix<'db>, + ) -> DecisionTree<'db> { + if matrix.rows_is_empty() { + let span = matrix.fail_span(span); + self.push(span, EmitDiagnosticKind::NonExhaustiveMatch); + return DecisionTree::Fail { span }; + } + if matrix.columns_is_empty() { + return matrix.into_first_leaf(); + } + if matrix.first_row_is_var_like() { + return matrix.into_var_like_leaf(); + } + + let state = matrix.into_selected_state(); + let first_col = state.first_col(); + + if let Some(fields) = self.product_column_fields(&state.test, &first_col) { + drop(first_col); + return self.compile_product_column(span, state, fields); + } + + let head_ctors = head_constructor_indices( + self.adt_layout_for_sem_ty(state.test.ty, state.test.span) + .as_ref(), + &first_col, + ); + if !head_ctors.is_empty() { + drop(first_col); + return self.compile_constructor_switch(span, state, head_ctors); + } + + let head_lits = head_literals(&first_col); + if !head_lits.is_empty() { + drop(first_col); + return self.compile_atomic_switch(span, state, head_lits); + } + + if first_col + .iter() + .any(|pat| matches!(pat, MatrixPat::ComptimeLabel)) + { + self.push( + span, + EmitDiagnosticKind::UnsupportedMonoConstruct { + construct: "unevaluated comptime match label".to_owned(), + }, + ); + return DecisionTree::Fail { span }; + } + + drop(first_col); + for row in &state.rows { + if let Some(pat) = row.pats.first() + && !pat.is_var_like() + { + self.push_discarded_match_pattern(row.span, pat, state.test.ty); + } + } + let (rows, columns) = state.into_default(); + self.compile_match_matrix(span, MatchMatrix::new(columns, rows)) + } + + fn product_column_fields( + &mut self, + test: &MatchColumn<'db>, + first_col: &[&MatrixPat], + ) -> Option>> { + let tuple_fields = first_col + .iter() + .any(|pat| matches!(pat, MatrixPat::Tuple { .. })) + .then(|| sem_product_fields_shallow(self.db, test.ty)); + let single_ctor_layout = self + .adt_layout_for_sem_ty(test.ty, test.span) + .filter(|layout| layout.ctors.len() == 1); + let fields = match (tuple_fields, single_ctor_layout) { + (Some(fields), _) => fields, + (None, Some(layout)) + if first_col + .iter() + .any(|pat| matches!(pat, MatrixPat::Con { .. })) => + { + let ctor = layout.ctors.into_iter().next()?; + ctor.fields + } + _ => return None, + }; + Some(fields) + } + + fn compile_product_column( + &mut self, + span: Span<'db>, + state: MatrixState<'db>, + fields: Vec>, + ) -> DecisionTree<'db> { + let MatrixState { test, rest, rows } = state; + + let child_columns = child_columns(&test.occurrence, &fields, test.span); + let mut next_columns = child_columns; + next_columns.extend(rest); + let mut next_rows = Vec::new(); + for row in rows { + let row_span = row.span; + let (first, row_rest) = split_row(row); + match first { + MatrixPat::Tuple { elems, .. } => { + next_rows.push(row_with_pats(row_rest, elems)); + } + MatrixPat::Con { ctor, args, .. } if self.single_ctor_matches(test.ty, &ctor) => { + next_rows.push(row_with_pats(row_rest, args)); + } + MatrixPat::Var { name, .. } => { + next_rows.push(row_with_binding_and_wildcards( + row_rest, + name, + test.occurrence.clone(), + fields.len(), + test.span, + )); + } + MatrixPat::Wildcard => { + next_rows.push(row_with_wildcards(row_rest, fields.len(), test.span)); + } + MatrixPat::Error => { + next_rows.push(row_with_wildcards(row_rest, fields.len(), test.span)); + } + unsupported @ (MatrixPat::Con { .. } + | MatrixPat::Lit { .. } + | MatrixPat::ComptimeLabel) => { + self.push_discarded_match_pattern(row_span, &unsupported, test.ty); + } + } + } + + let field_tys = fields + .iter() + .map(|field| self.hull_ty(*field, test.span)) + .collect(); + DecisionTree::Product { + occurrence: test.occurrence, + fields: field_tys, + subtree: Box::new( + self.compile_match_matrix(span, MatchMatrix::new(next_columns, next_rows)), + ), + } + } + + fn compile_constructor_switch( + &mut self, + span: Span<'db>, + state: MatrixState<'db>, + head_ctors: Vec, + ) -> DecisionTree<'db> { + let MatrixState { test, rest, rows } = state; + let Some(layout) = self.adt_layout_for_sem_ty(test.ty, test.span) else { + self.push( + test.span, + EmitDiagnosticKind::MissingAdtLayout { + adt: test.ty.display(self.db), + }, + ); + return DecisionTree::Fail { span }; + }; + for row in &rows { + let Some(pat) = row.pats.first() else { + continue; + }; + let supported = match pat { + MatrixPat::Con { ctor, .. } => constructor_index(&layout, ctor).is_some(), + MatrixPat::Var { .. } | MatrixPat::Wildcard | MatrixPat::Error => true, + MatrixPat::Tuple { .. } | MatrixPat::Lit { .. } | MatrixPat::ComptimeLabel => false, + }; + if !supported { + self.push_discarded_match_pattern(row.span, pat, test.ty); + } + } + let include_default = head_ctors.len() != layout.ctors.len(); + let (projected_branches, default_rows) = + project_constructor_rows(&test, &layout, &head_ctors, rows, include_default); + + let mut branches = Vec::new(); + for (index, next_rows) in head_ctors.iter().copied().zip(projected_branches) { + let ctor = &layout.ctors[index]; + let child_cols = child_columns(&test.occurrence, &ctor.fields, test.span); + let mut next_columns = child_cols; + next_columns.extend_from_slice(&rest); + branches.push(CtorDecision { + index, + tree: self.compile_match_matrix(span, MatchMatrix::new(next_columns, next_rows)), + }); + } + + let default = if !include_default { + None + } else if default_rows.is_empty() { + self.push(test.span, EmitDiagnosticKind::NonExhaustiveMatch); + Some(Box::new(DecisionTree::Fail { span: test.span })) + } else { + Some(Box::new(self.compile_match_matrix( + span, + MatchMatrix::new(rest, default_rows), + ))) + }; + + DecisionTree::Switch { + occurrence: test.occurrence, + layout, + branches, + default, + } + } + + fn compile_atomic_switch( + &mut self, + span: Span<'db>, + state: MatrixState<'db>, + head_lits: Vec, + ) -> DecisionTree<'db> { + let MatrixState { test, rest, rows } = state; + for row in &rows { + let Some(pat) = row.pats.first() else { + continue; + }; + let supported = match pat { + MatrixPat::Lit { lit } => { + matches!(lit, LitKind::Number(_) | LitKind::Hex(_)) && head_lits.contains(lit) + } + MatrixPat::Var { .. } | MatrixPat::Wildcard | MatrixPat::Error => true, + MatrixPat::Con { .. } | MatrixPat::Tuple { .. } | MatrixPat::ComptimeLabel => false, + }; + if !supported { + self.push_discarded_match_pattern(row.span, pat, test.ty); + } + } + let (projected_branches, default_rows) = project_atomic_rows(&test, &head_lits, rows); + + let mut branches = Vec::new(); + for (lit, next_rows) in head_lits.into_iter().zip(projected_branches) { + branches.push(AtomicDecision { + lit, + tree: self.compile_match_matrix(span, MatchMatrix::new(rest.clone(), next_rows)), + }); + } + + let default = if default_rows.is_empty() { + self.push(test.span, EmitDiagnosticKind::NonExhaustiveMatch); + Some(Box::new(DecisionTree::Fail { span: test.span })) + } else { + Some(Box::new(self.compile_match_matrix( + span, + MatchMatrix::new(rest, default_rows), + ))) + }; + + DecisionTree::AtomicSwitch { + occurrence: test.occurrence, + target: self.hull_ty(test.ty, test.span), + branches, + default, + } + } + + fn single_ctor_matches(&mut self, ty: SemTy<'db>, ctor: &str) -> bool { + self.adt_layout_for_sem_ty(ty, self.module.span(self.db)) + .filter(|layout| layout.ctors.len() == 1) + .is_some_and(|layout| { + constructor_name_matches(ctor, &layout.name, &layout.ctors[0].name) + }) + } + + fn push_discarded_match_pattern( + &mut self, + span: Span<'db>, + pat: &MatrixPat, + scrutinee_ty: SemTy<'db>, + ) { + let construct = match pat { + MatrixPat::Lit { + lit: LitKind::String(literal), + } => format!("string literal match pattern `{literal}`"), + MatrixPat::Lit { .. } => format!( + "literal match pattern incompatible with `{}`", + scrutinee_ty.display(self.db) + ), + MatrixPat::Con { ctor, .. } => format!( + "constructor match pattern `{ctor}` incompatible with `{}`", + scrutinee_ty.display(self.db) + ), + MatrixPat::Tuple { .. } => format!( + "tuple match pattern incompatible with `{}`", + scrutinee_ty.display(self.db) + ), + MatrixPat::ComptimeLabel => "unevaluated comptime match label".to_owned(), + MatrixPat::Wildcard | MatrixPat::Var { .. } | MatrixPat::Error => { + "invalid match pattern".to_owned() + } + }; + self.push( + span, + EmitDiagnosticKind::UnsupportedMonoConstruct { construct }, + ); + } + + fn tree_to_body( + &mut self, + span: Span<'db>, + occurrences: &mut BTreeMap>, + tree: &DecisionTree<'db>, + ) -> Vec> { + match tree { + DecisionTree::Leaf { bindings, body } => self.with_scope(|this| { + let mut materialized = Vec::new(); + for (name, occurrence) in bindings { + if let Some(expr) = occurrences.get(occurrence).cloned() { + let rhs = if super::emitter::expr_reads_var(&expr, name) { + let temp = this.fresh_temp("match_bind"); + materialized.push(Stmt { + span, + kind: StmtKind::Let { + name: temp.clone().into(), + ty: expr.ty.clone(), + }, + }); + materialized.push(Stmt { + span, + kind: StmtKind::Assign { + lhs: Expr::var(span, temp.clone(), expr.ty.clone()), + rhs: expr.clone(), + }, + }); + Expr::var(span, temp, expr.ty.clone()) + } else { + expr.clone() + }; + materialized.push(Stmt { + span, + kind: StmtKind::Let { + name: name.clone().into(), + ty: expr.ty.clone(), + }, + }); + materialized.push(Stmt { + span, + kind: StmtKind::Assign { + lhs: Expr::var(span, name.clone(), expr.ty.clone()), + rhs, + }, + }); + this.bind_expr(name.clone(), Expr::var(span, name.clone(), expr.ty)); + } + } + materialized.extend(this.emit_stmts(body)); + materialized + }), + DecisionTree::Fail { span } => vec![Stmt { + span: *span, + kind: StmtKind::Revert("non-exhaustive match".to_owned()), + }], + DecisionTree::Product { + occurrence, + fields, + subtree, + } => { + let Some(base) = occurrences.get(occurrence).cloned() else { + return vec![Stmt { + span, + kind: StmtKind::Revert("missing product occurrence".to_owned()), + }]; + }; + let mut next = occurrences.clone(); + for (index, expr) in product_field_exprs(base, fields).into_iter().enumerate() { + let mut child = occurrence.0.clone(); + child.push(index); + next.insert(Occurrence(child), expr); + } + self.tree_to_body(span, &mut next, subtree) + } + DecisionTree::Switch { + occurrence, + layout, + branches, + default, + } => { + let stmt = self.switch_tree_to_stmt( + span, + occurrences, + occurrence, + layout, + branches, + default.as_deref(), + ); + vec![stmt] + } + DecisionTree::AtomicSwitch { + occurrence, + target, + branches, + default, + } => { + let stmt = self.atomic_tree_to_stmt( + span, + occurrences, + occurrence, + target.clone(), + branches, + default.as_deref(), + ); + vec![stmt] + } + } + } + + fn switch_tree_to_stmt( + &mut self, + span: Span<'db>, + occurrences: &BTreeMap>, + occurrence: &Occurrence, + layout: &AdtLayout<'db>, + decisions: &[CtorDecision<'db>], + default: Option<&DecisionTree<'db>>, + ) -> Stmt<'db> { + let Some(scrutinee) = occurrences.get(occurrence).cloned() else { + return Stmt { + span, + kind: StmtKind::Revert("missing switch occurrence".to_owned()), + }; + }; + let mut branches = Vec::new(); + for (index, ctor) in layout.ctors.iter().enumerate() { + let binder = self.fresh_alt(); + let payload = Expr::var(span, binder.clone(), ctor.payload.clone()); + let body_tree = decisions + .iter() + .find(|decision| decision.index == index) + .map(|decision| &decision.tree) + .or(default); + let body = if let Some(tree) = body_tree { + let mut next = occurrences.clone(); + for (field_index, expr) in product_field_exprs( + payload.clone(), + &ctor + .fields + .iter() + .map(|field| self.hull_ty(*field, span)) + .collect::>(), + ) + .into_iter() + .enumerate() + { + let mut child = occurrence.0.clone(); + child.push(field_index); + next.insert(Occurrence(child), expr); + } + let mut body = self.tree_to_body(span, &mut next, tree); + if decisions.iter().any(|decision| decision.index == index) { + body.insert( + 0, + Stmt { + span, + kind: StmtKind::Comment(source_constructor_comment(&ctor.name)), + }, + ); + } + body + } else { + vec![Stmt { + span, + kind: StmtKind::Revert(format!("unreachable constructor: {}", ctor.name)), + }] + }; + branches.push(Branch { binder, body }); + } + build_nested_sum_match(span, scrutinee, layout.target.clone(), branches) + } + + fn atomic_tree_to_stmt( + &mut self, + span: Span<'db>, + occurrences: &mut BTreeMap>, + occurrence: &Occurrence, + target: Ty<'db>, + branches: &[AtomicDecision<'db>], + default: Option<&DecisionTree<'db>>, + ) -> Stmt<'db> { + let Some(scrutinee) = occurrences.get(occurrence).cloned() else { + return Stmt { + span, + kind: StmtKind::Revert("missing atomic occurrence".to_owned()), + }; + }; + let mut alts = branches + .iter() + .map(|branch| Alt { + span, + pat: Pat { + span, + kind: hull_lit_pat(&branch.lit), + }, + binder: self.fresh_alt().into(), + body: self.tree_to_body(span, occurrences, &branch.tree), + }) + .collect::>(); + if let Some(default) = default { + alts.push(Alt { + span, + pat: Pat { + span, + kind: PatKind::Wildcard, + }, + binder: self.fresh_alt().into(), + body: self.tree_to_body(span, occurrences, default), + }); + } + Stmt { + span, + kind: StmtKind::Match { + target, + scrutinee, + alts, + }, + } + } +} + +impl MatrixPat { + fn is_var_like(&self) -> bool { + matches!( + self, + MatrixPat::Wildcard | MatrixPat::Var { .. } | MatrixPat::Error + ) + } +} + +fn matrix_pat<'db>(db: &'db dyn hir_ty::Db, pat: &MonoPat<'db>) -> MatrixPat { + match &pat.kind { + MonoPatKind::Wildcard => MatrixPat::Wildcard, + MonoPatKind::Var(id) => MatrixPat::Var { + name: id.name.clone(), + }, + MonoPatKind::Lit(lit) => MatrixPat::Lit { + lit: wrap_word_lit_kind(lit), + }, + MonoPatKind::Con { ctor, args } + if ctor.is_builtin_ctor(db, MonoBuiltinCtor::Unit) && args.is_empty() => + { + MatrixPat::Tuple { elems: Vec::new() } + } + MonoPatKind::Con { ctor, args } + if ctor.is_builtin_ctor(db, MonoBuiltinCtor::Pair) && args.len() == 2 => + { + MatrixPat::Tuple { + elems: args.iter().map(|pat| matrix_pat(db, pat)).collect(), + } + } + MonoPatKind::Con { ctor, args } => MatrixPat::Con { + ctor: ctor.name.clone(), + args: args.iter().map(|pat| matrix_pat(db, pat)).collect(), + }, + MonoPatKind::Tuple(elems) => MatrixPat::Tuple { + elems: elems.iter().map(|pat| matrix_pat(db, pat)).collect(), + }, + MonoPatKind::ComptimeLabel(_) => MatrixPat::ComptimeLabel, + MonoPatKind::Error => MatrixPat::Error, + } +} + +fn sem_product_fields_shallow<'db>(db: &'db dyn hir_ty::Db, ty: SemTy<'db>) -> Vec> { + match ty.kind(db) { + SemTyKind::Tuple(elems) => elems.to_vec(), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Unit), + args, + } if args.is_empty() => Vec::new(), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => args.to_vec(), + _ => vec![ty], + } +} + +fn select_match_column<'db>(columns: &[MatchColumn<'db>], rows: &[MatchRow<'db>]) -> usize { + let mut best_index = 0; + let mut best_score = 0; + let mut best_depth = usize::MAX; + for (index, column) in columns.iter().enumerate() { + let score = rows + .iter() + .filter(|row| row.pats.get(index).is_some_and(|pat| !pat.is_var_like())) + .count(); + let depth = column.occurrence.0.len(); + if score > best_score || (score == best_score && depth < best_depth) { + best_index = index; + best_score = score; + best_depth = depth; + } + } + best_index +} + +fn move_selected_column_to_front<'db>(columns: &mut Vec>, selected: usize) { + if selected < columns.len() { + let column = columns.remove(selected); + columns.insert(0, column); + } +} + +fn move_selected_pat_to_front<'db>(rows: &mut [MatchRow<'db>], selected: usize) { + for row in rows { + if selected < row.pats.len() { + let pat = row.pats.remove(selected); + row.pats.insert(0, pat); + } + } +} + +fn split_row<'db>(mut row: MatchRow<'db>) -> (MatrixPat, MatchRow<'db>) { + let first = if row.pats.is_empty() { + MatrixPat::Wildcard + } else { + row.pats.remove(0) + }; + (first, row) +} + +fn row_with_pats<'db>(mut row: MatchRow<'db>, mut prefix: Vec) -> MatchRow<'db> { + prefix.extend(row.pats); + row.pats = prefix; + row +} + +fn row_with_wildcards<'db>(row: MatchRow<'db>, count: usize, _span: Span<'db>) -> MatchRow<'db> { + let wildcards = (0..count).map(|_| MatrixPat::Wildcard).collect::>(); + row_with_pats(row, wildcards) +} + +fn row_with_binding_and_wildcards<'db>( + mut row: MatchRow<'db>, + name: String, + occurrence: Occurrence, + count: usize, + span: Span<'db>, +) -> MatchRow<'db> { + row.bindings.push((name, occurrence)); + row_with_wildcards(row, count, span) +} + +fn project_constructor_rows<'db>( + test: &MatchColumn<'db>, + layout: &AdtLayout<'db>, + head_ctors: &[usize], + rows: Vec>, + include_default: bool, +) -> (Vec>>, Vec>) { + let mut branch_rows = (0..head_ctors.len()) + .map(|_| Vec::new()) + .collect::>(); + let mut default_rows = Vec::new(); + + for row in rows { + let (first, row_rest) = split_row(row); + match first { + MatrixPat::Con { + ctor: name, args, .. + } => { + let Some(index) = constructor_index(layout, &name) else { + continue; + }; + let Some(branch) = head_ctors.iter().position(|candidate| *candidate == index) + else { + continue; + }; + branch_rows[branch].push(row_with_pats(row_rest, args)); + } + MatrixPat::Var { name, .. } => { + push_constructor_var_rows( + test, + layout, + head_ctors, + &mut branch_rows, + include_default.then_some(&mut default_rows), + row_rest, + name, + ); + } + MatrixPat::Wildcard | MatrixPat::Error => { + push_constructor_wildcard_rows( + test, + layout, + head_ctors, + &mut branch_rows, + include_default.then_some(&mut default_rows), + row_rest, + ); + } + MatrixPat::Tuple { .. } | MatrixPat::Lit { .. } | MatrixPat::ComptimeLabel => {} + } + } + + (branch_rows, default_rows) +} + +fn push_constructor_var_rows<'db>( + test: &MatchColumn<'db>, + layout: &AdtLayout<'db>, + head_ctors: &[usize], + branch_rows: &mut [Vec>], + default_rows: Option<&mut Vec>>, + row_rest: MatchRow<'db>, + name: String, +) { + for (branch, index) in head_ctors.iter().copied().enumerate() { + let count = layout.ctors[index].fields.len(); + branch_rows[branch].push(row_with_binding_and_wildcards( + row_rest.clone(), + name.clone(), + test.occurrence.clone(), + count, + test.span, + )); + } + if let Some(default_rows) = default_rows { + let mut row = row_rest; + row.bindings.push((name, test.occurrence.clone())); + default_rows.push(row); + } +} + +fn push_constructor_wildcard_rows<'db>( + test: &MatchColumn<'db>, + layout: &AdtLayout<'db>, + head_ctors: &[usize], + branch_rows: &mut [Vec>], + default_rows: Option<&mut Vec>>, + row_rest: MatchRow<'db>, +) { + for (branch, index) in head_ctors.iter().copied().enumerate() { + let count = layout.ctors[index].fields.len(); + branch_rows[branch].push(row_with_wildcards(row_rest.clone(), count, test.span)); + } + if let Some(default_rows) = default_rows { + default_rows.push(row_rest); + } +} + +fn project_atomic_rows<'db>( + test: &MatchColumn<'db>, + head_lits: &[LitKind], + rows: Vec>, +) -> (Vec>>, Vec>) { + let mut branch_rows = (0..head_lits.len()).map(|_| Vec::new()).collect::>(); + let mut default_rows = Vec::new(); + + for row in rows { + let (first, row_rest) = split_row(row); + match first { + MatrixPat::Lit { lit: candidate, .. } => { + if let Some(branch) = head_lits.iter().position(|lit| lit == &candidate) { + branch_rows[branch].push(row_rest); + } + } + MatrixPat::Var { name, .. } => { + let mut row_rest = row_rest; + row_rest.bindings.push((name, test.occurrence.clone())); + push_projected_row(row_rest, &mut branch_rows, Some(&mut default_rows)); + } + MatrixPat::Wildcard | MatrixPat::Error => { + push_projected_row(row_rest, &mut branch_rows, Some(&mut default_rows)); + } + MatrixPat::Con { .. } | MatrixPat::Tuple { .. } | MatrixPat::ComptimeLabel => {} + } + } + + (branch_rows, default_rows) +} + +fn push_projected_row<'db>( + row: MatchRow<'db>, + branch_rows: &mut [Vec>], + default_rows: Option<&mut Vec>>, +) { + let Some((last_branch, prefix_branches)) = branch_rows.split_last_mut() else { + if let Some(default_rows) = default_rows { + default_rows.push(row); + } + return; + }; + for branch in prefix_branches { + branch.push(row.clone()); + } + if let Some(default_rows) = default_rows { + last_branch.push(row.clone()); + default_rows.push(row); + } else { + last_branch.push(row); + } +} + +fn default_rows<'db>( + occurrence: Occurrence, + rows: Vec>, + columns: Vec>, +) -> (Vec>, Vec>) { + let rows = rows + .into_iter() + .filter_map(|row| { + let (first, mut row) = split_row(row); + match first { + MatrixPat::Var { name, .. } => { + row.bindings.push((name, occurrence.clone())); + Some(row) + } + MatrixPat::Wildcard | MatrixPat::Error => Some(row), + MatrixPat::Lit { .. } + | MatrixPat::Con { .. } + | MatrixPat::Tuple { .. } + | MatrixPat::ComptimeLabel => None, + } + }) + .collect(); + (rows, columns) +} + +fn head_constructor_indices<'db>( + layout: Option<&AdtLayout<'db>>, + first_col: &[&MatrixPat], +) -> Vec { + let Some(layout) = layout else { + return Vec::new(); + }; + let mut out = Vec::new(); + for pat in first_col { + let MatrixPat::Con { ctor, .. } = pat else { + continue; + }; + let Some(index) = constructor_index(layout, ctor) else { + continue; + }; + if !out.contains(&index) { + out.push(index); + } + } + out +} + +fn head_literals(first_col: &[&MatrixPat]) -> Vec { + let mut out = Vec::new(); + for pat in first_col { + let MatrixPat::Lit { lit, .. } = pat else { + continue; + }; + if !matches!(lit, LitKind::Number(_) | LitKind::Hex(_)) { + continue; + } + if !out.contains(lit) { + out.push(lit.clone()); + } + } + out +} + +fn hull_lit_pat(lit: &LitKind) -> PatKind { + match lit { + LitKind::Number(value) | LitKind::Hex(value) => PatKind::IntLit(wrap_lit_text(value)), + LitKind::String(_) | LitKind::Error => PatKind::Wildcard, + } +} + +fn wrap_word_lit_kind(lit: &LitKind) -> LitKind { + match lit { + LitKind::Number(value) | LitKind::Hex(value) => { + LitKind::Number(canonical_word_literal(value).unwrap_or_else(|_| wrap_lit_text(value))) + } + LitKind::String(_) | LitKind::Error => lit.clone(), + } +} + +pub(super) fn wrap_lit_text(value: &str) -> String { + wrap_word_literal(value).unwrap_or_else(|_| value.to_owned()) +} + +fn child_columns<'db>( + occurrence: &Occurrence, + fields: &[SemTy<'db>], + span: Span<'db>, +) -> Vec> { + fields + .iter() + .enumerate() + .map(|(index, ty)| { + let mut child = occurrence.0.clone(); + child.push(index); + MatchColumn { + occurrence: Occurrence(child), + ty: *ty, + span, + } + }) + .collect() +} + +pub(super) fn encode_constructor<'db>( + span: Span<'db>, + target: Ty<'db>, + index: usize, + arity: usize, + payload: Expr<'db>, +) -> Expr<'db> { + if arity <= 1 { + let mut payload = payload; + payload.ty = target; + return payload; + } + if index == 0 { + Expr { + span, + ty: target.clone(), + kind: ExprKind::Inl { + target, + value: Box::new(payload), + }, + } + } else { + let right = sum_right_ty(&target); + let nested = encode_constructor(span, right, index - 1, arity - 1, payload); + Expr { + span, + ty: target.clone(), + kind: ExprKind::Inr { + target, + value: Box::new(nested), + }, + } + } +} + +fn build_nested_sum_match<'db>( + span: Span<'db>, + scrutinee: Expr<'db>, + target: Ty<'db>, + branches: Vec>, +) -> Stmt<'db> { + build_nested_sum_match_from_slice(span, scrutinee, target, &branches) +} + +fn build_nested_sum_match_from_slice<'db>( + span: Span<'db>, + scrutinee: Expr<'db>, + target: Ty<'db>, + branches: &[Branch<'db>], +) -> Stmt<'db> { + match branches { + [] => Stmt { + span, + kind: StmtKind::Revert("empty branch list".to_owned()), + }, + [branch] => Stmt { + span, + kind: StmtKind::Block(branch.body.clone()), + }, + [left, rest @ ..] => { + let right_ty = sum_right_ty(&target); + let right_binder = rest + .first() + .map(|branch| branch.binder.clone()) + .unwrap_or_else(|| "$alt".to_owned()); + let right_expr = Expr::var(span, right_binder.clone(), right_ty.clone()); + let rest_stmt = build_nested_sum_match_from_slice(span, right_expr, right_ty, rest); + Stmt { + span, + kind: StmtKind::Match { + target, + scrutinee, + alts: vec![ + Alt { + span, + pat: Pat { + span, + kind: PatKind::Con(Con::Inl), + }, + binder: left.binder.clone().into(), + body: left.body.clone(), + }, + Alt { + span, + pat: Pat { + span, + kind: PatKind::Con(Con::Inr), + }, + binder: right_binder.into(), + body: vec![rest_stmt], + }, + ], + }, + } + } + } +} + +pub(super) fn constructor_name_matches(actual: &str, adt: &str, ctor: &str) -> bool { + actual == ctor || actual == format!("{adt}_{ctor}") || actual.ends_with(&format!("_{ctor}")) +} + +pub(super) fn constructor_index(layout: &AdtLayout<'_>, actual: &str) -> Option { + let names = layout + .ctors + .iter() + .map(|ctor| ctor.name.as_str()) + .collect::>(); + constructor_index_from_names(actual, &layout.name, &names) +} + +fn constructor_index_from_names(actual: &str, adt: &str, ctors: &[&str]) -> Option { + ctors + .iter() + .position(|ctor| actual == *ctor || actual == format!("{adt}_{ctor}")) + .or_else(|| { + ctors + .iter() + .position(|ctor| actual.ends_with(&format!("_{ctor}"))) + }) +} + +fn source_constructor_comment(name: &str) -> String { + name.rsplit('_').next().unwrap_or(name).to_owned() +} + +#[cfg(test)] +mod tests { + use super::constructor_index_from_names; + + #[test] + fn constructor_lookup_prefers_full_names_over_suffixes() { + assert_eq!( + constructor_index_from_names("T_B_A", "T", &["A", "B_A"]), + Some(1) + ); + } +} diff --git a/crates/hull/src/emit/mod.rs b/crates/hull/src/emit/mod.rs new file mode 100644 index 00000000..b1ddac87 --- /dev/null +++ b/crates/hull/src/emit/mod.rs @@ -0,0 +1,87 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt, +}; + +use hir::{ + Db as HirDb, + anchor::DefId, + ast::{ + Ident, + function::{ + AssignOp, BinOp, LitKind, UnOp, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind, + }, + item::{AdtDef, ContractDef, ContractItem, Item, Module}, + ty::TypeRefKind, + }, + diag::{Diagnostic, DiagnosticCode}, + span::{Span, Spanned, SpannedElem}, +}; +use hir_ty::{ + BinderEnv, BuiltinTyCtor, ConversionKind, Ty as SemTy, TyCtor, TyKind as SemTyKind, + TypeLowering, UserTyCtorKind, contract::FrontendTransform, normalize_ty_aliases, + value_type_underlying, +}; +use parser::parse_file_to_hir; +use specialize::{ + MonoArm, MonoBuiltinCtor, MonoCallOrigin, MonoContract, MonoEntry, MonoExpr, MonoExprArm, + MonoExprKind, MonoFunction, MonoId, MonoIntrinsic, MonoItem, MonoModule, MonoPat, MonoPatKind, + MonoStmt, MonoStmtKind, +}; + +use crate::{ + ir::{ + Alt, Arg, CodeBlock, Con, Expr, ExprKind, Function, Object, Pat, PatKind, Program, Stmt, + StmtKind, Ty, TyKind, + }, + scope_stack::ScopeStack, + word::{canonical_word_literal, wrap_word_literal}, +}; + +mod contract; +mod diagnostics; +mod emitter; +mod layout; +mod match_compile; +mod reachability; +mod storage; +mod yul_build; + +use diagnostics::prune_emit_diagnostics; +pub use diagnostics::{EmitDiagnostic, EmitDiagnosticKind, EmitOptions, EmitOutput}; +pub use emitter::emit_module; +use layout::{ + bool_expr, hull_ty_word_slots, product_expr, product_field_exprs, sem_product_fields, + sem_ty_needs_untyped_word_default, sum_right_ty, +}; +use match_compile::{AdtLayout, CtorLayout, constructor_index, encode_constructor, wrap_lit_text}; +use reachability::deployment_closure; +use storage::StorageFieldKind; + +const STORAGE_INDEX_READ: &str = "__solcore_storage_index_read"; +const STORAGE_INDEX_SLOT: &str = "__solcore_storage_index_slot"; +const STORAGE_HASH2_HELPER: &str = "__solcore_storage_hash2"; +const STORAGE_MAPPING_VALUE_HELPER: &str = "__solcore_storage_mapping_value"; +/// Error selector of the reference std's `Unimplemented` error +/// (`Error(0x6e128399)` raised by `unimplemented()` in std.solc). +const UNIMPLEMENTED_SELECTOR: &str = "0x6e128399"; + +struct Emitter<'db> { + db: &'db dyn hir_ty::Db, + module: Module<'db>, + _options: EmitOptions, + diagnostics: Vec>, + scopes: ScopeStack>>, + function_names: BTreeSet, + layout_stack: Vec<(DefId<'db>, Vec>)>, + if_stmt_spans: Vec>, + predeclared_lets: Vec>, + fresh: usize, +} + +#[derive(Clone)] +struct PredeclaredLet<'db> { + span: Span<'db>, + backend_name: String, + ty: Ty<'db>, +} diff --git a/crates/hull/src/emit/reachability.rs b/crates/hull/src/emit/reachability.rs new file mode 100644 index 00000000..3a360801 --- /dev/null +++ b/crates/hull/src/emit/reachability.rs @@ -0,0 +1,191 @@ +use super::*; + +/// Names of all functions transitively reachable from the constructor set, +/// following both Hull-level calls and user-function calls inside assembly. +pub(super) fn deployment_closure<'db>( + db: &'db dyn hir_ty::Db, + functions: &[Function<'db>], + roots: &BTreeSet, +) -> BTreeSet { + let by_name: BTreeMap<&str, &Function<'db>> = functions + .iter() + .map(|function| (function.name.as_str(), function)) + .collect(); + let mut closed: BTreeSet = roots.clone(); + let mut work: Vec = roots.iter().cloned().collect(); + while let Some(name) = work.pop() { + let Some(function) = by_name.get(name.as_str()) else { + continue; + }; + let mut callees = BTreeSet::new(); + collect_body_callees(db, &function.body, &mut callees); + for callee in callees { + if by_name.contains_key(callee.as_str()) && closed.insert(callee.clone()) { + work.push(callee); + } + } + } + closed +} + +fn collect_body_callees<'db>( + db: &'db dyn hir_ty::Db, + body: &[Stmt<'db>], + out: &mut BTreeSet, +) { + for stmt in body { + collect_stmt_callees(db, stmt, out); + } +} + +fn collect_stmt_callees<'db>( + db: &'db dyn hir_ty::Db, + stmt: &Stmt<'db>, + out: &mut BTreeSet, +) { + match &stmt.kind { + StmtKind::Let { .. } | StmtKind::Break | StmtKind::Continue | StmtKind::Comment(_) => {} + StmtKind::Revert(_) => {} + StmtKind::Assign { lhs, rhs } => { + collect_expr_callees(lhs, out); + collect_expr_callees(rhs, out); + } + StmtKind::Expr(expr) | StmtKind::Return(expr) => collect_expr_callees(expr, out), + StmtKind::Block(stmts) => collect_body_callees(db, stmts, out), + StmtKind::For { + init, + cond, + post, + body, + } => { + collect_body_callees(db, init, out); + collect_expr_callees(cond, out); + collect_body_callees(db, post, out); + collect_body_callees(db, body, out); + } + StmtKind::Match { + scrutinee, alts, .. + } => { + collect_expr_callees(scrutinee, out); + for alt in alts { + collect_body_callees(db, &alt.body, out); + } + } + StmtKind::Assembly(stmts) => { + for stmt in stmts { + collect_yul_stmt_callees(db, stmt, out); + } + } + } +} + +fn collect_expr_callees<'db>(expr: &Expr<'db>, out: &mut BTreeSet) { + match &expr.kind { + ExprKind::Word(_) | ExprKind::Bool(_) | ExprKind::Unit | ExprKind::Var(_) => {} + ExprKind::Pair(lhs, rhs) => { + collect_expr_callees(lhs, out); + collect_expr_callees(rhs, out); + } + ExprKind::Fst(inner) | ExprKind::Snd(inner) => collect_expr_callees(inner, out), + ExprKind::Inl { value, .. } | ExprKind::Inr { value, .. } | ExprKind::InK { value, .. } => { + collect_expr_callees(value, out) + } + ExprKind::Call { callee, args } => { + out.insert(callee.as_str().to_owned()); + for arg in args { + collect_expr_callees(arg, out); + } + } + ExprKind::If { + cond, + then_expr, + else_expr, + .. + } => { + collect_expr_callees(cond, out); + collect_expr_callees(then_expr, out); + collect_expr_callees(else_expr, out); + } + } +} + +fn collect_yul_stmt_callees<'db>( + db: &'db dyn hir_ty::Db, + stmt: &hir::ast::function::YulStmt<'db>, + out: &mut BTreeSet, +) { + use hir::ast::function::YulStmtKind; + match &stmt.kind { + YulStmtKind::Block(stmts) => { + for stmt in stmts { + collect_yul_stmt_callees(db, stmt, out); + } + } + YulStmtKind::Let { init, .. } => { + if let Some(init) = init { + collect_yul_expr_callees(db, init, out); + } + } + YulStmtKind::Assign { value, .. } => collect_yul_expr_callees(db, value, out), + YulStmtKind::Expr(expr) => collect_yul_expr_callees(db, expr, out), + YulStmtKind::If { cond, body } => { + collect_yul_expr_callees(db, cond, out); + for stmt in body { + collect_yul_stmt_callees(db, stmt, out); + } + } + YulStmtKind::For { + init, + cond, + post, + body, + } => { + for stmt in init.iter().chain(post).chain(body) { + collect_yul_stmt_callees(db, stmt, out); + } + collect_yul_expr_callees(db, cond, out); + } + YulStmtKind::Switch { + expr, + cases, + default, + } => { + collect_yul_expr_callees(db, expr, out); + for case in cases { + for stmt in &case.body { + collect_yul_stmt_callees(db, stmt, out); + } + } + if let Some(default) = default { + for stmt in default { + collect_yul_stmt_callees(db, stmt, out); + } + } + } + YulStmtKind::FunctionDef { body, .. } => { + for stmt in body { + collect_yul_stmt_callees(db, stmt, out); + } + } + YulStmtKind::Leave | YulStmtKind::Break | YulStmtKind::Continue | YulStmtKind::Error => {} + } +} + +fn collect_yul_expr_callees<'db>( + db: &'db dyn hir_ty::Db, + expr: &hir::ast::function::YulExpr<'db>, + out: &mut BTreeSet, +) { + use hir::ast::function::YulExprKind; + match &expr.kind { + YulExprKind::Lit(_) | YulExprKind::Ident(_) | YulExprKind::Error => {} + YulExprKind::Call { name, args } => { + let text = (*name.atom()).text(db).to_owned(); + let text = text.strip_prefix("usr$").unwrap_or(&text).to_owned(); + out.insert(text); + for arg in args { + collect_yul_expr_callees(db, arg, out); + } + } + } +} diff --git a/crates/hull/src/emit/storage.rs b/crates/hull/src/emit/storage.rs new file mode 100644 index 00000000..b9540100 --- /dev/null +++ b/crates/hull/src/emit/storage.rs @@ -0,0 +1,931 @@ +use super::*; + +pub(super) struct StorageField { + slot: usize, + pub(super) kind: StorageFieldKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StorageFieldKind { + DirectWord, + Mapping, +} + +impl<'db> Emitter<'db> { + pub(super) fn contract_storage_fields( + &mut self, + def: DefId<'db>, + ) -> BTreeMap { + let module = parse_file_to_hir(self.db, def.file(self.db)).module(self.db); + let Some(contract) = find_contract(self.db, module, def) else { + return BTreeMap::new(); + }; + let resolutions = nameres::module_id_for_source_file(self.db, def.file(self.db)) + .map(|module_id| { + let env = nameres::module_env_for_hir_module(self.db, module_id, module); + let scope = env + .item_scope + .clone() + .unwrap_or_else(|| hir::nameres::item_scope(self.db, module)); + hir::nameres::resolve_item_types_with_imports(self.db, module, &scope, &env) + }) + .unwrap_or_else(|| hir::nameres::resolve_item_types(self.db, module)); + let lowerer = + TypeLowering::from_item_resolutions(self.db, &resolutions, BinderEnv::empty()); + let mut fields = BTreeMap::new(); + for (slot, field) in contract.fields(self.db).iter().enumerate() { + let source_ty = field.ty(); + let ty = lowerer.lower_field(field).ty; + let normalized = normalize_ty_aliases(self.db, module, &resolutions, ty).value; + let source_contains_fixed_array = type_ref_contains_fixed_array(self.db, source_ty); + let semantic_contains_fixed_array = self.storage_sem_ty_contains_fixed_array( + normalized, + &mut std::collections::HashSet::new(), + ); + let kind = if source_contains_fixed_array || semantic_contains_fixed_array { + self.push( + source_ty.span(self.db), + EmitDiagnosticKind::UnsupportedType { + ty: if semantic_contains_fixed_array { + normalized.display(self.db) + } else { + "fixed-length array nested in a contract-storage field".to_owned() + }, + }, + ); + None + } else if let Some(kind) = field_storage_kind(self.db, source_ty) { + Some(kind) + } else { + self.user_adt_storage_field_kind(normalized, source_ty.span(self.db)) + }; + if let Some(kind) = kind { + fields.insert( + field.name().atom().text(self.db).to_owned(), + StorageField { slot, kind }, + ); + } + } + fields + } + + fn user_adt_storage_field_kind( + &mut self, + ty: SemTy<'db>, + span: Span<'db>, + ) -> Option { + if matches!( + ty.kind(self.db), + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } + ) { + self.push( + span, + EmitDiagnosticKind::UnsupportedType { + ty: ty.display(self.db), + }, + ); + return None; + } + let SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } = ty.kind(self.db) + else { + return None; + }; + match user.kind { + UserTyCtorKind::ValueType if args.is_empty() => { + let underlying = value_type_underlying(self.db, user.def).ok()?; + let lowered = self.try_hull_ty(underlying, span); + if lowered + .as_ref() + .is_some_and(|ty| matches!(ty.strip_named().kind, TyKind::Word)) + { + return Some(StorageFieldKind::DirectWord); + } + self.push( + span, + EmitDiagnosticKind::UnsupportedType { + ty: ty.display(self.db), + }, + ); + None + } + UserTyCtorKind::Adt => { + let ty = self.try_hull_ty(ty, span)?; + (hull_ty_word_slots(&ty) == Some(1)).then_some(StorageFieldKind::DirectWord) + } + UserTyCtorKind::Alias | UserTyCtorKind::Contract | UserTyCtorKind::ValueType => None, + } + } + + fn storage_sem_ty_contains_fixed_array( + &self, + ty: SemTy<'db>, + visiting_adts: &mut std::collections::HashSet>, + ) -> bool { + match ty.kind(self.db) { + SemTyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => true, + SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } if matches!(user.kind, UserTyCtorKind::Adt) => { + if args + .iter() + .any(|arg| self.storage_sem_ty_contains_fixed_array(*arg, visiting_adts)) + { + return true; + } + if !visiting_adts.insert(user.def) { + return false; + } + let module = parse_file_to_hir(self.db, user.def.file(self.db)).module(self.db); + let contains = layout::find_adt(self.db, module, user.def) + .and_then(|adt| hir_ty::derived_generic_plan(self.db, module, adt)) + .is_some_and(|plan| { + let rep = layout::subst_sem_ty(self.db, plan.rep, args); + self.storage_sem_ty_contains_fixed_array(rep, visiting_adts) + }); + visiting_adts.remove(&user.def); + contains + } + SemTyKind::Named { + ctor: TyCtor::User(user), + args, + } if matches!(user.kind, UserTyCtorKind::ValueType) && args.is_empty() => { + value_type_underlying(self.db, user.def).is_ok_and(|underlying| { + self.storage_sem_ty_contains_fixed_array(underlying, visiting_adts) + }) + } + SemTyKind::Named { args, .. } | SemTyKind::Tuple(args) => args + .iter() + .any(|arg| self.storage_sem_ty_contains_fixed_array(*arg, visiting_adts)), + SemTyKind::Function { params, ret } => { + params + .iter() + .any(|param| self.storage_sem_ty_contains_fixed_array(*param, visiting_adts)) + || self.storage_sem_ty_contains_fixed_array(*ret, visiting_adts) + } + SemTyKind::Comptime(inner) => { + self.storage_sem_ty_contains_fixed_array(*inner, visiting_adts) + } + SemTyKind::Error | SemTyKind::Unknown | SemTyKind::BoundVar(_) => false, + } + } + + pub(super) fn lower_storage_fields_in_function( + &self, + mut function: Function<'db>, + fields: &BTreeMap, + storage_hash_helper: Option<&str>, + mapping_value_helper_used: &mut bool, + ) -> Function<'db> { + if fields.is_empty() { + return function; + } + let mut lowerer = StorageLowerer::new(self, fields, storage_hash_helper, &function.args); + function.body = lowerer.stmts(function.body); + *mapping_value_helper_used |= lowerer.mapping_value_helper_used; + function + } + + pub(super) fn storage_hash2_function(&self, span: Span<'db>, name: &str) -> Function<'db> { + let word = Ty::word(span); + Function { + span, + name: name.into(), + args: vec![ + Arg { + span, + name: "x".into(), + ty: word.clone(), + }, + Arg { + span, + name: "y".into(), + ty: word.clone(), + }, + ], + ret: word.clone(), + body: vec![ + Stmt { + span, + kind: StmtKind::Let { + name: "out".into(), + ty: word.clone(), + }, + }, + self.assembly_stmt( + span, + vec![ + self.yul_expr_stmt( + span, + self.yul_call( + span, + "mstore", + vec![self.yul_number(span, "0"), self.yul_ident_expr(span, "x")], + ), + ), + self.yul_expr_stmt( + span, + self.yul_call( + span, + "mstore", + vec![self.yul_number(span, "32"), self.yul_ident_expr(span, "y")], + ), + ), + self.yul_assign( + span, + "out", + self.yul_call( + span, + "keccak256", + vec![self.yul_number(span, "0"), self.yul_number(span, "64")], + ), + ), + ], + ), + Stmt { + span, + kind: StmtKind::Return(Expr::var(span, "out", word)), + }, + ], + } + } + + /// Mirrors the reference std's `storage(mapping(k, v)) : CanStore` + /// instance, whose `load`/`store` bodies are `unimplemented()`: touching a + /// whole mapping field as a value compiles, but reverts at runtime with + /// the std `Unimplemented` error, nominally yielding the field's base + /// slot (the storage reference). + pub(super) fn storage_mapping_value_function( + &self, + span: Span<'db>, + name: &str, + ) -> Function<'db> { + let word = Ty::word(span); + Function { + span, + name: name.into(), + args: vec![Arg { + span, + name: "slot".into(), + ty: word.clone(), + }], + ret: word.clone(), + body: vec![ + self.assembly_stmt( + span, + vec![ + self.yul_expr_stmt( + span, + self.yul_call( + span, + "mstore", + vec![ + self.yul_number(span, "0"), + self.yul_number(span, UNIMPLEMENTED_SELECTOR), + ], + ), + ), + self.yul_expr_stmt( + span, + self.yul_call( + span, + "revert", + vec![self.yul_number(span, "28"), self.yul_number(span, "4")], + ), + ), + ], + ), + Stmt { + span, + kind: StmtKind::Return(Expr::var(span, "slot", word)), + }, + ], + } + } +} + +fn type_ref_contains_fixed_array(db: &dyn HirDb, ty: hir::ast::ty::TypeRef<'_>) -> bool { + match ty.kind(db) { + TypeRefKind::FixedArray { .. } => true, + TypeRefKind::Named { args, .. } => args + .atom() + .iter() + .any(|arg| type_ref_contains_fixed_array(db, *arg)), + TypeRefKind::Fn { params, ret, .. } => { + params + .atom() + .iter() + .any(|param| type_ref_contains_fixed_array(db, *param)) + || type_ref_contains_fixed_array(db, *ret) + } + TypeRefKind::Comptime { inner, .. } => type_ref_contains_fixed_array(db, *inner), + TypeRefKind::Tuple { elems } => elems + .atom() + .iter() + .any(|elem| type_ref_contains_fixed_array(db, *elem)), + TypeRefKind::Error { .. } => false, + } +} + +struct StorageLowerer<'a, 'db> { + emitter: &'a Emitter<'db>, + fields: &'a BTreeMap, + storage_hash_helper: Option<&'a str>, + shadows: ScopeStack>, + fresh: usize, + mapping_value_helper_used: bool, +} + +impl<'a, 'db> StorageLowerer<'a, 'db> { + fn new( + emitter: &'a Emitter<'db>, + fields: &'a BTreeMap, + storage_hash_helper: Option<&'a str>, + args: &[Arg<'db>], + ) -> Self { + Self { + emitter, + fields, + storage_hash_helper, + shadows: ScopeStack::new_root_with_message( + args.iter() + .map(|arg| arg.name.as_str().to_owned()) + .collect(), + "storage scope stack is never empty", + ), + fresh: 0, + mapping_value_helper_used: false, + } + } + + fn stmts(&mut self, stmts: Vec>) -> Vec> { + let mut out = Vec::new(); + for stmt in stmts { + out.extend(self.stmt(stmt)); + } + out + } + + fn stmt(&mut self, stmt: Stmt<'db>) -> Vec> { + match stmt.kind { + StmtKind::Let { name, ty } => { + self.shadows.last_mut().insert(name.as_str().to_owned()); + vec![Stmt { + span: stmt.span, + kind: StmtKind::Let { name, ty }, + }] + } + StmtKind::Assign { lhs, rhs } => { + if let ExprKind::Var(name) = &lhs.kind + && let Some(slot) = self.direct_field(name.as_str()).map(|field| field.slot) + { + let rhs = self.expr(rhs); + let temp = self.fresh_temp(name.as_str()); + return vec![ + Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: temp.clone().into(), + ty: lhs.ty.clone(), + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, temp.clone(), lhs.ty), + rhs, + }, + }, + self.emitter.assembly_stmt( + stmt.span, + vec![self.emitter.yul_expr_stmt( + stmt.span, + self.emitter.yul_call( + stmt.span, + "sstore", + vec![ + self.emitter.yul_number(stmt.span, slot.to_string()), + self.emitter.yul_ident_expr(stmt.span, &temp), + ], + ), + )], + ), + ]; + } + if let ExprKind::Var(name) = &lhs.kind + && let Some(slot) = self.mapping_field(name.as_str()).map(|field| field.slot) + { + // A whole mapping field as an assignment target: the + // reference compiles this via `CanStore.store`, which + // evaluates the rhs and then hits an `unimplemented()` + // runtime trap. + self.mapping_value_helper_used = true; + let rhs = self.expr(rhs); + let temp = self.fresh_temp(name.as_str()); + let trap = self.fresh_temp(name.as_str()); + let word = Ty::word(stmt.span); + return vec![ + Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: temp.clone().into(), + ty: lhs.ty.clone(), + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, temp, lhs.ty), + rhs, + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: trap.clone().into(), + ty: word.clone(), + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, trap, word.clone()), + rhs: Expr { + span: stmt.span, + ty: word, + kind: ExprKind::Call { + callee: STORAGE_MAPPING_VALUE_HELPER.into(), + args: vec![Expr::word(stmt.span, slot.to_string())], + }, + }, + }, + }, + ]; + } + if let Some(slot) = self.storage_index_read_slot(&lhs) { + let lowered_slot = self.expr(slot.clone()); + let slot_temp = self.fresh_temp("storage_index_slot"); + let slot_ref = Expr::var(stmt.span, slot_temp.clone(), Ty::word(stmt.span)); + let rhs = replace_storage_index_read_slot(rhs, &slot, &slot_ref); + let rhs = self.expr(rhs); + let value_temp = self.fresh_temp("storage_index"); + return vec![ + Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: slot_temp.clone().into(), + ty: Ty::word(stmt.span), + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: slot_ref.clone(), + rhs: lowered_slot, + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Let { + name: value_temp.clone().into(), + ty: lhs.ty.clone(), + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: Expr::var(stmt.span, value_temp.clone(), lhs.ty), + rhs, + }, + }, + Stmt { + span: stmt.span, + kind: StmtKind::Expr(Expr { + span: stmt.span, + ty: Ty::unit(stmt.span), + kind: ExprKind::Call { + callee: "sstore".into(), + args: vec![ + slot_ref, + Expr::var(stmt.span, value_temp, Ty::word(stmt.span)), + ], + }, + }), + }, + ]; + } + vec![Stmt { + span: stmt.span, + kind: StmtKind::Assign { + lhs: self.expr(lhs), + rhs: self.expr(rhs), + }, + }] + } + StmtKind::Expr(expr) => vec![Stmt { + span: stmt.span, + kind: StmtKind::Expr(self.expr(expr)), + }], + StmtKind::Return(expr) => vec![Stmt { + span: stmt.span, + kind: StmtKind::Return(self.expr(expr)), + }], + StmtKind::Block(body) => self.with_scope(|this| { + vec![Stmt { + span: stmt.span, + kind: StmtKind::Block(this.stmts(body)), + }] + }), + StmtKind::For { + init, + cond, + post, + body, + } => self.with_scope(|this| { + let init = this.stmts(init); + let cond = this.expr(cond); + let post = this.stmts(post); + let body = this.stmts(body); + vec![Stmt { + span: stmt.span, + kind: StmtKind::For { + init, + cond, + post, + body, + }, + }] + }), + StmtKind::Match { + target, + scrutinee, + alts, + } => { + let scrutinee = self.expr(scrutinee); + let alts = alts + .into_iter() + .map(|alt| self.alt(alt)) + .collect::>(); + vec![Stmt { + span: stmt.span, + kind: StmtKind::Match { + target, + scrutinee, + alts, + }, + }] + } + kind @ (StmtKind::Assembly(_) + | StmtKind::Revert(_) + | StmtKind::Comment(_) + | StmtKind::Break + | StmtKind::Continue) => vec![Stmt { + span: stmt.span, + kind, + }], + } + } + + fn alt(&mut self, alt: Alt<'db>) -> Alt<'db> { + self.with_scope(|this| { + this.shadows + .last_mut() + .insert(alt.binder.as_str().to_owned()); + Alt { + span: alt.span, + pat: alt.pat, + binder: alt.binder, + body: this.stmts(alt.body), + } + }) + } + + fn expr(&mut self, expr: Expr<'db>) -> Expr<'db> { + match expr.kind { + ExprKind::Var(name) => { + if let Some(slot) = self.direct_field(name.as_str()).map(|field| field.slot) { + Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Call { + callee: "sload".into(), + args: vec![Expr::word(expr.span, slot.to_string())], + }, + } + } else if let Some(slot) = self.mapping_field(name.as_str()).map(|field| field.slot) + { + // A whole mapping field read as a value: the reference + // compiles this via `CanStore.load`, which is an + // `unimplemented()` runtime trap returning the base slot. + self.mapping_value_helper_used = true; + Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Call { + callee: STORAGE_MAPPING_VALUE_HELPER.into(), + args: vec![Expr::word(expr.span, slot.to_string())], + }, + } + } else { + Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Var(name), + } + } + } + ExprKind::Call { callee, args } + if callee.as_str() == STORAGE_INDEX_READ && args.len() == 1 => + { + let mut args = args.into_iter(); + let slot = self.expr(args.next().expect("checked len")); + Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Call { + callee: "sload".into(), + args: vec![slot], + }, + } + } + ExprKind::Call { callee, args } + if callee.as_str() == STORAGE_INDEX_SLOT && args.len() == 2 => + { + let mut args = args.into_iter(); + let base = args.next().expect("checked len"); + let index = args.next().expect("checked len"); + self.storage_index_slot_expr(expr.span, expr.ty, base, index) + } + ExprKind::Pair(lhs, rhs) => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Pair(Box::new(self.expr(*lhs)), Box::new(self.expr(*rhs))), + }, + ExprKind::Fst(inner) => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Fst(Box::new(self.expr(*inner))), + }, + ExprKind::Snd(inner) => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Snd(Box::new(self.expr(*inner))), + }, + ExprKind::Inl { target, value } => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Inl { + target, + value: Box::new(self.expr(*value)), + }, + }, + ExprKind::Inr { target, value } => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Inr { + target, + value: Box::new(self.expr(*value)), + }, + }, + ExprKind::InK { + index, + target, + value, + } => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::InK { + index, + target, + value: Box::new(self.expr(*value)), + }, + }, + ExprKind::Call { callee, args } => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Call { + callee, + args: args.into_iter().map(|arg| self.expr(arg)).collect(), + }, + }, + ExprKind::If { + target, + cond, + then_expr, + else_expr, + } => Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::If { + target, + cond: Box::new(self.expr(*cond)), + then_expr: Box::new(self.expr(*then_expr)), + else_expr: Box::new(self.expr(*else_expr)), + }, + }, + ExprKind::Word(_) | ExprKind::Bool(_) | ExprKind::Unit => expr, + } + } + + fn field(&self, name: &str) -> Option<&StorageField> { + if self.shadows.iter().rev().any(|scope| scope.contains(name)) { + return None; + } + self.fields.get(name) + } + + fn direct_field(&self, name: &str) -> Option<&StorageField> { + self.field(name) + .filter(|field| field.kind == StorageFieldKind::DirectWord) + } + + fn mapping_field(&self, name: &str) -> Option<&StorageField> { + self.field(name) + .filter(|field| field.kind == StorageFieldKind::Mapping) + } + + fn storage_index_read_slot(&self, expr: &Expr<'db>) -> Option> { + let ExprKind::Call { callee, args } = &expr.kind else { + return None; + }; + if callee.as_str() != STORAGE_INDEX_READ || args.len() != 1 { + return None; + } + args.first().cloned() + } + + fn storage_index_slot_expr( + &mut self, + span: Span<'db>, + ty: Ty<'db>, + base: Expr<'db>, + index: Expr<'db>, + ) -> Expr<'db> { + let base = self.storage_slot_base_expr(base); + let index = self.expr(index); + Expr { + span, + ty, + kind: ExprKind::Call { + callee: self + .storage_hash_helper + .unwrap_or(STORAGE_HASH2_HELPER) + .into(), + args: vec![base, index], + }, + } + } + + fn storage_slot_base_expr(&mut self, base: Expr<'db>) -> Expr<'db> { + match base.kind { + ExprKind::Var(name) => { + if let Some(slot) = self.field(name.as_str()).map(|field| field.slot) { + Expr::word(base.span, slot.to_string()) + } else { + Expr { + span: base.span, + ty: base.ty, + kind: ExprKind::Var(name), + } + } + } + ExprKind::Call { callee, args } + if callee.as_str() == STORAGE_INDEX_SLOT && args.len() == 2 => + { + let mut args = args.into_iter(); + let nested_base = args.next().expect("checked len"); + let nested_index = args.next().expect("checked len"); + self.storage_index_slot_expr(base.span, base.ty, nested_base, nested_index) + } + _ => self.expr(base), + } + } + + fn fresh_temp(&mut self, field: &str) -> String { + let name = format!("storage_store_{field}_{}", self.fresh); + self.fresh += 1; + name + } + + fn with_scope(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + self.shadows.push(BTreeSet::new()); + let out = f(self); + let _ = self.shadows.pop(); + out + } +} + +fn replace_storage_index_read_slot<'db>( + expr: Expr<'db>, + slot: &Expr<'db>, + slot_ref: &Expr<'db>, +) -> Expr<'db> { + if let ExprKind::Call { callee, args } = &expr.kind + && callee.as_str() == STORAGE_INDEX_READ + && args.len() == 1 + && args.first() == Some(slot) + { + return Expr { + span: expr.span, + ty: expr.ty, + kind: ExprKind::Call { + callee: "sload".into(), + args: vec![slot_ref.clone()], + }, + }; + } + + Expr { + span: expr.span, + ty: expr.ty, + kind: match expr.kind { + ExprKind::Pair(lhs, rhs) => ExprKind::Pair( + Box::new(replace_storage_index_read_slot(*lhs, slot, slot_ref)), + Box::new(replace_storage_index_read_slot(*rhs, slot, slot_ref)), + ), + ExprKind::Fst(inner) => ExprKind::Fst(Box::new(replace_storage_index_read_slot( + *inner, slot, slot_ref, + ))), + ExprKind::Snd(inner) => ExprKind::Snd(Box::new(replace_storage_index_read_slot( + *inner, slot, slot_ref, + ))), + ExprKind::Inl { target, value } => ExprKind::Inl { + target, + value: Box::new(replace_storage_index_read_slot(*value, slot, slot_ref)), + }, + ExprKind::Inr { target, value } => ExprKind::Inr { + target, + value: Box::new(replace_storage_index_read_slot(*value, slot, slot_ref)), + }, + ExprKind::InK { + index, + target, + value, + } => ExprKind::InK { + index, + target, + value: Box::new(replace_storage_index_read_slot(*value, slot, slot_ref)), + }, + ExprKind::Call { callee, args } => ExprKind::Call { + callee, + args: args + .into_iter() + .map(|arg| replace_storage_index_read_slot(arg, slot, slot_ref)) + .collect(), + }, + ExprKind::If { + target, + cond, + then_expr, + else_expr, + } => ExprKind::If { + target, + cond: Box::new(replace_storage_index_read_slot(*cond, slot, slot_ref)), + then_expr: Box::new(replace_storage_index_read_slot(*then_expr, slot, slot_ref)), + else_expr: Box::new(replace_storage_index_read_slot(*else_expr, slot, slot_ref)), + }, + ExprKind::Word(value) => ExprKind::Word(value), + ExprKind::Bool(value) => ExprKind::Bool(value), + ExprKind::Unit => ExprKind::Unit, + ExprKind::Var(name) => ExprKind::Var(name), + }, + } +} + +fn field_storage_kind<'db>( + db: &'db dyn HirDb, + ty: hir::ast::ty::TypeRef<'db>, +) -> Option { + let TypeRefKind::Named { name, args, .. } = ty.kind(db) else { + return None; + }; + let name = name.atom().text(db); + if args.atom().is_empty() && matches!(name, "word" | "uint" | "uint256" | "bytes32" | "address") + { + return Some(StorageFieldKind::DirectWord); + } + if name == "mapping" && args.atom().len() == 2 { + return Some(StorageFieldKind::Mapping); + } + None +} + +fn find_contract<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(*contract), + _ => None, + }) +} diff --git a/crates/hull/src/emit/yul_build.rs b/crates/hull/src/emit/yul_build.rs new file mode 100644 index 00000000..7d6b4c18 --- /dev/null +++ b/crates/hull/src/emit/yul_build.rs @@ -0,0 +1,65 @@ +use super::*; + +impl<'db> Emitter<'db> { + pub(super) fn assembly_stmt(&self, span: Span<'db>, body: Vec>) -> Stmt<'db> { + Stmt { + span, + kind: StmtKind::Assembly(body), + } + } + + pub(super) fn yul_assign( + &self, + span: Span<'db>, + name: &str, + value: YulExpr<'db>, + ) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Assign { + names: vec![self.yul_ident(span, name)], + value, + }, + } + } + + pub(super) fn yul_expr_stmt(&self, span: Span<'db>, expr: YulExpr<'db>) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Expr(expr), + } + } + + pub(super) fn yul_call( + &self, + span: Span<'db>, + name: &str, + args: Vec>, + ) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Call { + name: self.yul_ident(span, name), + args, + }, + } + } + + pub(super) fn yul_number(&self, span: Span<'db>, value: impl Into) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Lit(YulLitKind::Number(value.into())), + } + } + + pub(super) fn yul_ident_expr(&self, span: Span<'db>, name: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Ident(self.yul_ident(span, name)), + } + } + + pub(super) fn yul_ident(&self, span: Span<'db>, name: &str) -> SpannedElem<'db, Ident<'db>> { + SpannedElem::new(Ident::new(self.db, name.to_owned()), span) + } +} diff --git a/crates/hull/src/ir.rs b/crates/hull/src/ir.rs new file mode 100644 index 00000000..9039bcf2 --- /dev/null +++ b/crates/hull/src/ir.rs @@ -0,0 +1,325 @@ +use hir::{ast::function::YulStmt, span::Span}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HullName(String); + +impl HullName { + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for HullName { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for HullName { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } +} + +impl std::fmt::Display for HullName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +pub type Name = HullName; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Program<'db> { + pub span: Span<'db>, + /// Top-level functions that may be invoked as object-less program entries. + pub entry_points: Vec, + pub functions: Vec>, + pub objects: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Ty<'db> { + pub span: Span<'db>, + pub kind: TyKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TyKind<'db> { + Word, + Bool, + Unit, + Product(Box>, Box>), + Sum(Box>, Box>), + Named { + name: Name, + inner: Box>, + }, + NamedRef { + name: Name, + }, + Function { + params: Vec>, + ret: Box>, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Expr<'db> { + pub span: Span<'db>, + pub ty: Ty<'db>, + pub kind: ExprKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExprKind<'db> { + Word(String), + Bool(bool), + Unit, + Var(Name), + Pair(Box>, Box>), + Fst(Box>), + Snd(Box>), + Inl { + target: Ty<'db>, + value: Box>, + }, + Inr { + target: Ty<'db>, + value: Box>, + }, + InK { + index: usize, + target: Ty<'db>, + value: Box>, + }, + Call { + callee: Name, + args: Vec>, + }, + If { + target: Ty<'db>, + cond: Box>, + then_expr: Box>, + else_expr: Box>, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Stmt<'db> { + pub span: Span<'db>, + pub kind: StmtKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StmtKind<'db> { + Let { + name: Name, + ty: Ty<'db>, + }, + Assign { + lhs: Expr<'db>, + rhs: Expr<'db>, + }, + Expr(Expr<'db>), + Return(Expr<'db>), + Block(Vec>), + For { + init: Vec>, + cond: Expr<'db>, + post: Vec>, + body: Vec>, + }, + Break, + Continue, + Match { + target: Ty<'db>, + scrutinee: Expr<'db>, + alts: Vec>, + }, + Assembly(Vec>), + Revert(String), + Comment(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Alt<'db> { + pub span: Span<'db>, + pub pat: Pat<'db>, + pub binder: Name, + pub body: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pat<'db> { + pub span: Span<'db>, + pub kind: PatKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PatKind { + Var(Name), + Con(Con), + Wildcard, + IntLit(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Con { + Inl, + Inr, + InK(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Arg<'db> { + pub span: Span<'db>, + pub name: Name, + pub ty: Ty<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Function<'db> { + pub span: Span<'db>, + pub name: Name, + pub args: Vec>, + pub ret: Ty<'db>, + pub body: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CodeBlock<'db> { + pub span: Span<'db>, + pub stmts: Vec>, + pub functions: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Object<'db> { + pub span: Span<'db>, + pub name: Name, + pub code: CodeBlock<'db>, + pub inners: Vec>, +} + +impl<'db> Ty<'db> { + pub fn word(span: Span<'db>) -> Self { + Self { + span, + kind: TyKind::Word, + } + } + + pub fn bool(span: Span<'db>) -> Self { + Self { + span, + kind: TyKind::Bool, + } + } + + pub fn unit(span: Span<'db>) -> Self { + Self { + span, + kind: TyKind::Unit, + } + } + + pub fn product(span: Span<'db>, lhs: Ty<'db>, rhs: Ty<'db>) -> Self { + Self { + span, + kind: TyKind::Product(Box::new(lhs), Box::new(rhs)), + } + } + + pub fn sum(span: Span<'db>, lhs: Ty<'db>, rhs: Ty<'db>) -> Self { + Self { + span, + kind: TyKind::Sum(Box::new(lhs), Box::new(rhs)), + } + } + + pub fn named(span: Span<'db>, name: impl Into, inner: Ty<'db>) -> Self { + Self { + span, + kind: TyKind::Named { + name: name.into(), + inner: Box::new(inner), + }, + } + } + + pub fn named_ref(span: Span<'db>, name: impl Into) -> Self { + Self { + span, + kind: TyKind::NamedRef { name: name.into() }, + } + } + + pub fn function(span: Span<'db>, params: Vec>, ret: Ty<'db>) -> Self { + Self { + span, + kind: TyKind::Function { + params, + ret: Box::new(ret), + }, + } + } + + pub fn strip_named(&self) -> &Self { + match &self.kind { + TyKind::Named { inner, .. } => inner.strip_named(), + _ => self, + } + } + + pub fn contains_function(&self) -> bool { + match &self.kind { + TyKind::Function { .. } => true, + TyKind::Product(lhs, rhs) | TyKind::Sum(lhs, rhs) => { + lhs.contains_function() || rhs.contains_function() + } + TyKind::Named { inner, .. } => inner.contains_function(), + TyKind::NamedRef { .. } | TyKind::Word | TyKind::Bool | TyKind::Unit => false, + } + } +} + +impl<'db> Expr<'db> { + pub fn var(span: Span<'db>, name: impl Into, ty: Ty<'db>) -> Self { + Self { + span, + ty, + kind: ExprKind::Var(name.into()), + } + } + + pub fn unit(span: Span<'db>) -> Self { + Self { + span, + ty: Ty::unit(span), + kind: ExprKind::Unit, + } + } + + pub fn word(span: Span<'db>, value: impl Into) -> Self { + Self { + span, + ty: Ty::word(span), + kind: ExprKind::Word(value.into()), + } + } +} + +impl Con { + pub fn as_str(self) -> &'static str { + match self { + Self::Inl => "inl", + Self::Inr => "inr", + Self::InK(_) => "in", + } + } +} diff --git a/crates/hull/src/lib.rs b/crates/hull/src/lib.rs new file mode 100644 index 00000000..ca12dd4b --- /dev/null +++ b/crates/hull/src/lib.rs @@ -0,0 +1,24 @@ +//! Hull IR, emission, validation, and concrete-syntax printing. +//! +//! Hull is the first-order monomorphic backend IR used after specialization. +//! The emitter consumes [`specialize::MonoModule`] and preserves the +//! anchor-relative [`hir::span::Span`] values already attached to the mono IR. +//! ADT layout is recovered through `hir-ty`'s derived generic representation +//! plan so constructor payload products and right-nested sums share the same +//! encoding source of truth as generated `Generic.from`/`Generic.to` code. + +mod check; +mod emit; +mod ir; +mod pretty; +mod scope_stack; +mod word; + +pub use check::{CheckDiagnostic, CheckDiagnosticKind, check_program, check_program_with_db}; +pub use emit::{EmitDiagnostic, EmitDiagnosticKind, EmitOptions, EmitOutput, emit_module}; +pub use ir::{ + Alt, Arg, CodeBlock, Con, Expr, ExprKind, Function, HullName, Name, Object, Pat, PatKind, + Program, Stmt, StmtKind, Ty, TyKind, +}; +pub use pretty::{PrettyHull, pretty_program}; +pub use word::{WordLiteralError, wrap_word_literal}; diff --git a/crates/hull/src/pretty.rs b/crates/hull/src/pretty.rs new file mode 100644 index 00000000..189db91a --- /dev/null +++ b/crates/hull/src/pretty.rs @@ -0,0 +1,480 @@ +use std::fmt::Write as _; + +use hir::{ + Db as HirDb, + ast::function::{YulCase, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind}, +}; + +use crate::ir::{ + Alt, Arg, CodeBlock, Con, Expr, ExprKind, Function, Object, Pat, PatKind, Program, Stmt, + StmtKind, Ty, TyKind, +}; + +pub trait PrettyHull<'db> { + fn to_hull_string(&self, db: &'db dyn HirDb) -> String; +} + +pub fn pretty_program<'db>(db: &'db dyn HirDb, program: &Program<'db>) -> String { + program.to_hull_string(db) +} + +impl<'db> PrettyHull<'db> for Program<'db> { + fn to_hull_string(&self, db: &'db dyn HirDb) -> String { + let mut out = String::new(); + for (index, function) in self.functions.iter().enumerate() { + if index > 0 { + out.push('\n'); + } + write_function(db, &mut out, function, 0); + } + if !self.functions.is_empty() && !self.objects.is_empty() { + out.push('\n'); + } + for (index, object) in self.objects.iter().enumerate() { + if index > 0 { + out.push('\n'); + } + write_object(db, &mut out, object, 0); + } + out + } +} + +impl<'db> PrettyHull<'db> for Ty<'db> { + fn to_hull_string(&self, _db: &'db dyn HirDb) -> String { + write_ty(self) + } +} + +impl<'db> PrettyHull<'db> for Expr<'db> { + fn to_hull_string(&self, _db: &'db dyn HirDb) -> String { + write_expr(self) + } +} + +fn write_object<'db>(db: &'db dyn HirDb, out: &mut String, object: &Object<'db>, indent: usize) { + line( + out, + indent, + &format!("object \"{}\" {{", escape_string(object.name.as_str())), + ); + line(out, indent + 1, "code {"); + write_code_block(db, out, &object.code, indent + 2); + line(out, indent + 1, "}"); + for inner in &object.inners { + write_object(db, out, inner, indent + 1); + } + line(out, indent, "}"); +} + +fn write_code_block<'db>( + db: &'db dyn HirDb, + out: &mut String, + code: &CodeBlock<'db>, + indent: usize, +) { + for function in &code.functions { + write_function(db, out, function, indent); + } + for stmt in &code.stmts { + write_stmt(db, out, stmt, indent); + } +} + +fn write_function<'db>( + db: &'db dyn HirDb, + out: &mut String, + function: &Function<'db>, + indent: usize, +) { + let args = function + .args + .iter() + .map(write_arg) + .collect::>() + .join(", "); + line( + out, + indent, + &format!( + "function {} ({}) -> {} {{", + function.name, + args, + write_ty(&function.ret) + ), + ); + for stmt in &function.body { + write_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); +} + +fn write_arg<'db>(arg: &Arg<'db>) -> String { + format!("{} : {}", arg.name, write_ty(&arg.ty)) +} + +fn write_stmt<'db>(db: &'db dyn HirDb, out: &mut String, stmt: &Stmt<'db>, indent: usize) { + match &stmt.kind { + StmtKind::Let { name, ty } => line(out, indent, &format!("let {name} : {}", write_ty(ty))), + StmtKind::Assign { lhs, rhs } => line( + out, + indent, + &format!("{} := {}", write_expr(lhs), write_expr(rhs)), + ), + StmtKind::Expr(expr) => line(out, indent, &write_expr(expr)), + StmtKind::Return(expr) => line(out, indent, &format!("return {}", write_expr(expr))), + StmtKind::Block(stmts) => { + line(out, indent, "{"); + for stmt in stmts { + write_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + StmtKind::For { + init, + cond, + post, + body, + } => { + line( + out, + indent, + &format!( + "for ({}; {}; {}) {{", + write_stmt_list_inline(init), + write_expr(cond), + write_stmt_list_inline(post) + ), + ); + for stmt in body { + write_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + StmtKind::Break => line(out, indent, "break"), + StmtKind::Continue => line(out, indent, "continue"), + StmtKind::Match { + target, + scrutinee, + alts, + } => { + line( + out, + indent, + &format!( + "match<{}> {} with {{", + write_ty(target), + write_expr(scrutinee) + ), + ); + for alt in alts { + write_alt(db, out, alt, indent + 1); + } + line(out, indent, "}"); + } + StmtKind::Assembly(stmts) => { + line(out, indent, "assembly {"); + for stmt in stmts { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + StmtKind::Revert(message) => { + line( + out, + indent, + &format!("revertLit \"{}\"", escape_string(message)), + ); + } + StmtKind::Comment(comment) => { + line( + out, + indent, + &format!("/* {} */", comment.replace("*/", "* /")), + ); + } + } +} + +fn write_stmt_list_inline(stmts: &[Stmt<'_>]) -> String { + match stmts { + [] => "{}".to_owned(), + [stmt] => write_stmt_inline(stmt), + _ => { + let body = stmts + .iter() + .map(write_stmt_inline) + .collect::>() + .join(" "); + format!("{{ {body} }}") + } + } +} + +fn write_stmt_inline(stmt: &Stmt<'_>) -> String { + match &stmt.kind { + StmtKind::Let { name, ty } => format!("let {name} : {}", write_ty(ty)), + StmtKind::Assign { lhs, rhs } => format!("{} := {}", write_expr(lhs), write_expr(rhs)), + StmtKind::Expr(expr) => write_expr(expr), + StmtKind::Return(expr) => format!("return {}", write_expr(expr)), + StmtKind::Block(stmts) => { + if stmts.is_empty() { + "{}".to_owned() + } else { + let body = stmts + .iter() + .map(write_stmt_inline) + .collect::>() + .join(" "); + format!("{{ {body} }}") + } + } + StmtKind::Break => "break".to_owned(), + StmtKind::Continue => "continue".to_owned(), + StmtKind::Revert(message) => format!("revertLit \"{}\"", escape_string(message)), + StmtKind::Comment(comment) => format!("/* {} */", comment.replace("*/", "* /")), + StmtKind::For { .. } | StmtKind::Match { .. } | StmtKind::Assembly(_) => "{}".to_owned(), + } +} + +fn write_alt<'db>(db: &'db dyn HirDb, out: &mut String, alt: &Alt<'db>, indent: usize) { + line( + out, + indent, + &format!("{} {} => {{", write_pat(&alt.pat), alt.binder), + ); + for stmt in &alt.body { + write_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); +} + +fn write_ty<'db>(ty: &Ty<'db>) -> String { + match &ty.kind { + TyKind::Word => "word".to_owned(), + TyKind::Bool => "bool".to_owned(), + TyKind::Unit => "unit".to_owned(), + TyKind::Product(lhs, rhs) => format!("({} * {})", write_ty(lhs), write_ty(rhs)), + TyKind::Sum(lhs, rhs) => format!("({} + {})", write_ty(lhs), write_ty(rhs)), + TyKind::Named { name, inner } => format!("{name}{{{}}}", write_ty(inner)), + TyKind::NamedRef { name } => name.as_str().to_owned(), + TyKind::Function { params, ret } => { + let params = params.iter().map(write_ty).collect::>().join(", "); + format!("({params} -> {})", write_ty(ret)) + } + } +} + +fn write_expr<'db>(expr: &Expr<'db>) -> String { + match &expr.kind { + ExprKind::Word(value) => value.clone(), + ExprKind::Bool(value) => value.to_string(), + ExprKind::Unit => "()".to_owned(), + ExprKind::Var(name) => name.as_str().to_owned(), + ExprKind::Pair(lhs, rhs) => format!("({}, {})", write_expr(lhs), write_expr(rhs)), + ExprKind::Fst(expr) => format!("fst({})", write_expr(expr)), + ExprKind::Snd(expr) => format!("snd({})", write_expr(expr)), + ExprKind::Inl { target, value } => { + format!("inl<{}>({})", write_ty(target), write_expr(value)) + } + ExprKind::Inr { target, value } => { + format!("inr<{}>({})", write_ty(target), write_expr(value)) + } + ExprKind::InK { + index, + target, + value, + } => format!("in({index})<{}>({})", write_ty(target), write_expr(value)), + ExprKind::Call { callee, args } => { + let args = args.iter().map(write_expr).collect::>().join(", "); + format!("{callee}({args})") + } + ExprKind::If { + target, + cond, + then_expr, + else_expr, + } => format!( + "if<{}> {} then ({}) else ({})", + write_ty(target), + write_expr(cond), + write_expr(then_expr), + write_expr(else_expr) + ), + } +} + +fn write_pat(pat: &Pat<'_>) -> String { + match &pat.kind { + PatKind::Var(name) => name.as_str().to_owned(), + PatKind::Con(con) => match con { + Con::Inl => "inl".to_owned(), + Con::Inr => "inr".to_owned(), + Con::InK(index) => format!("in({index})"), + }, + PatKind::Wildcard => "_".to_owned(), + PatKind::IntLit(value) => value.clone(), + } +} + +fn write_yul_stmt<'db>(db: &'db dyn HirDb, out: &mut String, stmt: &YulStmt<'db>, indent: usize) { + match &stmt.kind { + YulStmtKind::Block(stmts) => { + line(out, indent, "{"); + for stmt in stmts { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + YulStmtKind::Let { names, init } => { + let names = yul_names(db, names); + match init { + Some(init) => line( + out, + indent, + &format!("let {names} := {}", yul_expr(db, init)), + ), + None => line(out, indent, &format!("let {names}")), + } + } + YulStmtKind::Assign { names, value } => line( + out, + indent, + &format!("{} := {}", yul_names(db, names), yul_expr(db, value)), + ), + YulStmtKind::Expr(expr) => line(out, indent, &yul_expr(db, expr)), + YulStmtKind::If { cond, body } => { + line(out, indent, &format!("if {} {{", yul_expr(db, cond))); + for stmt in body { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + YulStmtKind::For { + init, + cond, + post, + body, + } => { + line(out, indent, "for {"); + for stmt in init { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, &format!("}} {} {{", yul_expr(db, cond))); + for stmt in post { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "} {"); + for stmt in body { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + YulStmtKind::Switch { + expr, + cases, + default, + } => { + line(out, indent, &format!("switch {}", yul_expr(db, expr))); + for case in cases { + write_yul_case(db, out, case, indent + 1); + } + if let Some(default) = default { + line(out, indent + 1, "default {"); + for stmt in default { + write_yul_stmt(db, out, stmt, indent + 2); + } + line(out, indent + 1, "}"); + } + } + YulStmtKind::FunctionDef { + name, + params, + rets, + body, + } => { + let name = (*name.atom()).text(db); + let params = yul_names(db, params); + let rets = yul_names(db, rets); + let ret = if rets.is_empty() { + String::new() + } else { + format!(" -> {rets}") + }; + line(out, indent, &format!("function {name}({params}){ret} {{")); + for stmt in body { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); + } + YulStmtKind::Leave => line(out, indent, "leave"), + YulStmtKind::Break => line(out, indent, "break"), + YulStmtKind::Continue => line(out, indent, "continue"), + YulStmtKind::Error => line(out, indent, ""), + } +} + +fn write_yul_case<'db>(db: &'db dyn HirDb, out: &mut String, case: &YulCase<'db>, indent: usize) { + line(out, indent, &format!("case {} {{", yul_lit(&case.lit))); + for stmt in &case.body { + write_yul_stmt(db, out, stmt, indent + 1); + } + line(out, indent, "}"); +} + +fn yul_names<'db>( + db: &'db dyn HirDb, + names: &[hir::span::SpannedElem<'db, hir::ast::Ident<'db>>], +) -> String { + names + .iter() + .map(|name| (*name.atom()).text(db).to_owned()) + .collect::>() + .join(", ") +} + +fn yul_expr<'db>(db: &'db dyn HirDb, expr: &YulExpr<'db>) -> String { + match &expr.kind { + YulExprKind::Lit(lit) => yul_lit(lit), + YulExprKind::Ident(name) => (*name.atom()).text(db).to_owned(), + YulExprKind::Call { name, args } => { + let name = (*name.atom()).text(db); + let args = args + .iter() + .map(|arg| yul_expr(db, arg)) + .collect::>() + .join(", "); + format!("{name}({args})") + } + YulExprKind::Error => "".to_owned(), + } +} + +fn yul_lit(lit: &YulLitKind) -> String { + match lit { + YulLitKind::Number(value) | YulLitKind::Hex(value) | YulLitKind::String(value) => { + value.clone() + } + YulLitKind::Bool(value) => value.to_string(), + YulLitKind::Error => "".to_owned(), + } +} + +fn line(out: &mut String, indent: usize, text: &str) { + let _ = writeln!(out, "{}{text}", " ".repeat(indent)); +} + +fn escape_string(value: &str) -> String { + let mut out = String::new(); + for ch in value.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + ch => out.push(ch), + } + } + out +} diff --git a/crates/hull/src/scope_stack.rs b/crates/hull/src/scope_stack.rs new file mode 100644 index 00000000..d46c9c2d --- /dev/null +++ b/crates/hull/src/scope_stack.rs @@ -0,0 +1,33 @@ +pub(crate) struct ScopeStack { + scopes: Vec, + empty_message: &'static str, +} + +impl ScopeStack { + pub(crate) fn new_root(root: T) -> Self { + Self::new_root_with_message(root, "scope stack is never empty") + } + + pub(crate) fn new_root_with_message(root: T, empty_message: &'static str) -> Self { + Self { + scopes: vec![root], + empty_message, + } + } + + pub(crate) fn push(&mut self, scope: T) { + self.scopes.push(scope); + } + + pub(crate) fn pop(&mut self) -> T { + self.scopes.pop().expect(self.empty_message) + } + + pub(crate) fn last_mut(&mut self) -> &mut T { + self.scopes.last_mut().expect(self.empty_message) + } + + pub(crate) fn iter(&self) -> std::slice::Iter<'_, T> { + self.scopes.iter() + } +} diff --git a/crates/hull/src/word.rs b/crates/hull/src/word.rs new file mode 100644 index 00000000..093ebbc6 --- /dev/null +++ b/crates/hull/src/word.rs @@ -0,0 +1,149 @@ +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WordLiteralError { + message: String, +} + +impl WordLiteralError { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for WordLiteralError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for WordLiteralError {} + +pub fn wrap_word_literal(value: &str) -> Result { + let word = parse_word_literal(value)?; + if word.overflow { + Ok(word.to_decimal_string()) + } else { + Ok(value.to_owned()) + } +} + +/// Returns the decimal spelling of a word literal's value modulo 2^256. +pub(crate) fn canonical_word_literal(value: &str) -> Result { + Ok(parse_word_literal(value)?.to_decimal_string()) +} + +fn parse_word_literal(value: &str) -> Result { + let (digits, radix) = if let Some(digits) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + if digits.is_empty() || !digits.chars().all(|ch| ch.is_ascii_hexdigit()) { + return Err(WordLiteralError::new(format!( + "invalid hex word literal `{value}`" + ))); + } + (digits, 16) + } else { + if value.is_empty() || !value.chars().all(|ch| ch.is_ascii_digit()) { + return Err(WordLiteralError::new(format!( + "invalid decimal word literal `{value}`" + ))); + } + (value, 10) + }; + + let mut word = Word256::default(); + for ch in digits.chars() { + let digit = ch.to_digit(radix).expect("literal digit was validated"); + word.mul_add_small(radix, digit); + } + Ok(word) +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +struct Word256 { + limbs: [u32; 8], + overflow: bool, +} + +impl Word256 { + fn mul_add_small(&mut self, base: u32, digit: u32) { + let mut carry = u64::from(digit); + for limb in &mut self.limbs { + let value = u64::from(*limb) * u64::from(base) + carry; + *limb = value as u32; + carry = value >> 32; + } + if carry != 0 { + self.overflow = true; + } + } + + fn to_decimal_string(&self) -> String { + if self.limbs.iter().all(|limb| *limb == 0) { + return "0".to_owned(); + } + + let mut limbs = self.limbs; + let mut digits = Vec::new(); + while limbs.iter().any(|limb| *limb != 0) { + let rem = div_rem_small(&mut limbs, 10); + digits.push((b'0' + rem as u8) as char); + } + digits.iter().rev().collect() + } +} + +fn div_rem_small(limbs: &mut [u32; 8], divisor: u32) -> u32 { + let mut rem = 0u64; + for limb in limbs.iter_mut().rev() { + let value = (rem << 32) | u64::from(*limb); + *limb = (value / u64::from(divisor)) as u32; + rem = value % u64::from(divisor); + } + rem as u32 +} + +#[cfg(test)] +mod tests { + use super::{canonical_word_literal, wrap_word_literal}; + + const TWO_256: &str = + "115792089237316195423570985008687907853269984665640564039457584007913129639936"; + const TWO_256_PLUS_ONE: &str = + "115792089237316195423570985008687907853269984665640564039457584007913129639937"; + + #[test] + fn wraps_out_of_range_words() { + assert_eq!(wrap_word_literal(TWO_256).unwrap(), "0"); + assert_eq!(wrap_word_literal(TWO_256_PLUS_ONE).unwrap(), "1"); + assert_eq!( + wrap_word_literal( + "0x10000000000000000000000000000000000000000000000000000000000000000" + ) + .unwrap(), + "0" + ); + } + + #[test] + fn keeps_in_range_spelling_unchanged() { + assert_eq!(wrap_word_literal("42").unwrap(), "42"); + assert_eq!(wrap_word_literal("0042").unwrap(), "0042"); + assert_eq!(wrap_word_literal("0X2a").unwrap(), "0X2a"); + } + + #[test] + fn canonicalizes_equal_spellings_to_the_same_decimal_word() { + assert_eq!(canonical_word_literal("0x10").unwrap(), "16"); + assert_eq!(canonical_word_literal("0016").unwrap(), "16"); + assert_eq!(canonical_word_literal(TWO_256_PLUS_ONE).unwrap(), "1"); + } +} diff --git a/crates/hull/tests/smoke.rs b/crates/hull/tests/smoke.rs new file mode 100644 index 00000000..19704a0a --- /dev/null +++ b/crates/hull/tests/smoke.rs @@ -0,0 +1,2136 @@ +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + fs, + path::{Path, PathBuf}, +}; + +use hir::{anchor::DefLocationTable, ast::item::Module, input::SourceFile}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, module_key_for_path, module_path_display, resolve_module_path_candidate, +}; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::Setter; +use solcore_hull::{ + CheckDiagnosticKind, EmitDiagnostic, EmitDiagnosticKind, EmitOptions, check_program_with_db, + emit_module, pretty_program, +}; +use specialize::{SpecializeOptions, SpecializeOutput, specialize_module}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, +} + +impl TestDb { + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.unwrap_or_else(|| { + ModuleTree::new( + self, + PathBuf::from("/main"), + PathBuf::from("/std"), + BTreeMap::new(), + ) + }) + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .unwrap_or_else(|| ModuleFsSnapshot::new(self, BTreeSet::new(), BTreeMap::new())) + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .unwrap_or_else(|| ModuleFileSnapshot::new(self, BTreeMap::new())) + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl hir_ty::Db for TestDb {} + +#[test] +fn specialization_corpus_subset_emits_and_checks() { + let cases = [ + ( + "spec/01id", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/01id.solc"), + ), + ( + "spec/00answer", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/00answer.solc"), + ), + ( + "spec/022add", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/022add.solc"), + ), + ( + "spec/024arith", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/024arith.solc"), + ), + ( + "spec/031maybe", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/031maybe.solc"), + ), + ( + "spec/047rgb", + include_str!("../../parser/tests/fixtures/corpus/ok/test/examples/spec/047rgb.solc"), + ), + ]; + let mut failures = Vec::new(); + for (name, src) in cases { + let (db, output) = specialize_src(name, src); + if !output.diagnostics.is_empty() { + failures.push(format!( + "{name}: specialize: {}", + output + .diagnostics + .iter() + .map(|diagnostic| format!("{:?}", diagnostic.kind)) + .collect::>() + .join("; ") + )); + continue; + } + let emitted = emit_module( + db, + &output.module, + EmitOptions { + emit_dispatcher_comments: false, + }, + ); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + if !non_dispatch.is_empty() { + failures.push(format!( + "{name}: emit: {}", + non_dispatch + .into_iter() + .map(|diagnostic| format!("{:?}", diagnostic.kind)) + .collect::>() + .join("; ") + )); + continue; + } + let checked = check_program_with_db(db, &emitted.program); + if !checked.is_empty() { + failures.push(format!( + "{name}: check: {}", + checked + .iter() + .map(|diagnostic| format!("{:?}", diagnostic.kind)) + .collect::>() + .join("; ") + )); + } + } + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + +#[test] +fn contract_without_runtime_main_defers_dispatch_to_specialization() { + let (db, output) = specialize_src( + "dispatch_word", + r#" +contract C { + function main() returns () {} +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let mut module = output.module; + for item in &mut module.items { + if let specialize::MonoItem::Contract(contract) = item { + contract + .entries + .retain(|entry| !matches!(entry, specialize::MonoEntry::RuntimeMain { .. })); + } + } + let emitted = emit_module(db, &module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::DispatcherDeferred { contract } if contract == "C" + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("calldataload(0)"), "{hull}"); + assert!(!hull.contains("dispatch_selector"), "{hull}"); +} + +#[test] +fn dispatch_basic_fixture_uses_std_dispatch_main() { + let fixture = repo_root() + .join("crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/basic.solc"); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + assert!(hull.contains("basic_C_main_"), "{hull}"); + assert!(hull.contains("dispatch_selector_matches"), "{hull}"); + assert!( + hull.contains("std_abi_decode_d") + && hull.contains("$calldata_") + && hull.contains("memory_") + && hull.contains("string_"), + "{hull}" + ); + assert!(hull.contains("opcodes_mcopy"), "{hull}"); + assert!(!hull.contains("dispatch_ret12_abi_head0_offset"), "{hull}"); +} + +#[test] +fn identity_conversion_emits_without_runtime_operation() { + let hull = pretty_src_hull( + "identity_conversion", + r#" +alias W = word; + +function identity(x: word) returns (word) { + return x as W as word; +} + +contract C { + value: word; + + function main() public returns (word) { + return identity(value); + } +} +"#, + ); + let identity = hull_function(&hull, "_identity_"); + assert!(identity.contains("return x"), "{identity}\n{hull}"); +} + +#[test] +fn value_type_wrap_and_unwrap_emit_without_runtime_operation() { + let hull = pretty_src_hull( + "value_type_conversion", + r#" +type Wad is word; + +function roundtrip(x: word) returns (word) { + return (x as Wad) as word; +} + +contract C { + value: word; + + function main() public returns (word) { + return roundtrip(value); + } +} +"#, + ); + let roundtrip = hull_function(&hull, "_roundtrip_"); + assert!(roundtrip.contains("return x"), "{roundtrip}\n{hull}"); +} + +#[test] +fn value_type_unwraps_to_alias_targets_emit_with_canonical_layouts() { + let hull = pretty_src_hull_with_std( + "value_type_alias_targets", + r#" +import std; +import {calldataload} from std.opcodes; + +alias W = word; +alias B = bool; + +type Wad is word; +type Flag is bool; + +function unwrap_word(x: Wad) returns (W) { + return x as W; +} + +function unwrap_bool(x: Flag) returns (B) { + return x as B; +} + +contract C { + value: word; + + function main() public returns (word) { + let raw: word; + assembly { raw := calldataload(0) } + let flag: Flag = (tobool(raw)) as Flag; + if (unwrap_bool(flag)) { + return unwrap_word(value as Wad); + } else { + return 0; + } + } +} +"#, + ); + let unwrap_word = hull_function(&hull, "_unwrap_word_"); + let unwrap_bool = hull_function(&hull, "_unwrap_bool_"); + assert!(unwrap_word.contains("return x"), "{unwrap_word}\n{hull}"); + assert!(unwrap_bool.contains("return x"), "{unwrap_bool}\n{hull}"); +} + +#[test] +fn deployment_objects_copy_runtime_and_guard_constructor_value() { + let repo = repo_root(); + let fixture = repo.join( + "crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty_no_constructor.solc", + ); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + assert!(hull.contains("object \"CDeploy\""), "{hull}"); + assert!(hull.contains("object \"C\""), "{hull}"); + assert!( + hull.contains("codecopy(0, dataoffset(\"C\"), datasize(\"C\"))"), + "{hull}" + ); + + let fixture = repo + .join("crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/nonpayable_ctor.solc"); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + let outer = hull + .split("object \"NonPayableCtor\" {") + .next() + .expect("outer object"); + assert!(outer.contains("object \"NonPayableCtorDeploy\""), "{hull}"); + assert!(outer.contains("mstore(64, memoryguard(128))"), "{hull}"); + assert!( + outer.contains("datasize(\"NonPayableCtorDeploy\")"), + "{hull}" + ); + assert!(outer.contains("if callvalue()"), "{hull}"); + assert!(outer.contains("0xb5988ea3"), "{hull}"); + assert!(outer.matches("_start").count() >= 2, "{hull}"); + assert!( + outer.contains("codecopy(0, dataoffset(\"NonPayableCtor\"), datasize(\"NonPayableCtor\"))"), + "{hull}" + ); + assert!(outer.contains("return(0, size)"), "{hull}"); + let runtime = hull + .split("object \"NonPayableCtor\" {") + .nth(1) + .expect("runtime object"); + assert!(!runtime.contains("_start"), "{hull}"); + + let fixture = repo + .join("crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable_ctor.solc"); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + let hull = pretty_program(db, &emitted.program); + let outer = hull + .split("object \"PayableCtor\" {") + .next() + .expect("outer object"); + assert!(!outer.contains("0xb5988ea3"), "{hull}"); +} + +#[test] +fn importless_nullary_constructor_uses_overlay_deployment_entry() { + let (db, output) = specialize_src( + "nullary_ctor_overlay", + r#" +contract C { + constructor() {} + + function main() returns () { + return (); + } +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + let outer = hull.split("object \"C\" {").next().expect("outer object"); + assert!(outer.contains("_start"), "{hull}"); + assert!(outer.contains("init_"), "{hull}"); + assert!( + outer.contains("codecopy(0, dataoffset(\"C\"), datasize(\"C\"))"), + "{hull}" + ); + assert!(!outer.contains("constructor_arg"), "{hull}"); +} + +#[test] +fn std_constructor_overlay_decodes_appended_arguments_in_deployment_closure() { + let (db, output) = specialize_src_with_std( + "std_ctor_overlay_args", + r#" +import std; +import std.dispatch; + +contract C { + constructor(config: uint256) { + let saved_config = config; + } + + function echo(config: uint256) public returns (uint256) { return config; } +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + let outer = hull.split("object \"C\" {").next().expect("outer object"); + assert!(outer.contains("copy_arguments_for_constructor"), "{hull}"); + assert!(outer.contains("abi_decode"), "{hull}"); + assert!(outer.contains("MemoryWordReader"), "{hull}"); + assert!( + outer.contains("argSize := sub(codesize(), programSize)"), + "{hull}" + ); + assert!(!outer.contains("minimumSize"), "{hull}"); + assert!(!outer.contains("BoundedMemoryWordReader"), "{hull}"); + assert!( + outer.contains("codecopy(memoryDataOffset, programSize, argSize)"), + "{hull}" + ); + assert!( + !outer.contains("codecopy(0, datasize(\"CDeploy\"), 32)"), + "{hull}" + ); + assert!(!outer.contains("constructor_arg"), "{hull}"); + assert!(outer.matches("_start").count() >= 2, "{hull}"); + + let runtime = hull.split("object \"C\" {").nth(1).expect("runtime object"); + assert!( + !runtime.contains("copy_arguments_for_constructor"), + "{hull}" + ); + assert!(!runtime.contains("_start"), "{hull}"); +} + +#[test] +fn std_dispatch_address_decode_rejects_dirty_high_bits() { + let (db, output) = specialize_src_with_std( + "std_address_dispatch", + r#" +import std; +import std.dispatch; + +contract C { + function id_address(a: address) public returns (address) { return a; } +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + assert!( + hull.contains("ABIDecode_decode_d") + && hull.contains("$ABIDecoder_") + && hull.contains("address_"), + "{hull}" + ); + assert!(hull.contains("(160, raw)"), "{hull}"); + assert!(hull.contains("0x7cc04fa7"), "{hull}"); +} + +#[test] +fn std_dispatch_explicit_fallback_stops_after_execution() { + let (db, output) = specialize_src_with_std( + "std_fallback_dispatch", + r#" +import std; +import std.dispatch; + +contract C { + function answer() public returns (uint256) { return uint256.uint256(42); } + fallback() external {} +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + let hull = pretty_program(db, &emitted.program); + assert!(hull.contains("stop()"), "{hull}"); +} + +#[test] +fn for_loop_emits_hull_for_and_loop_control() { + let repo = repo_root(); + let fixture = + repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-break.solc"); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + !emitted.diagnostics.iter().any(|diagnostic| { + matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedMonoConstruct { construct } + if construct == "for loop" || construct == "loop control" + ) + }), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(hull.contains("for ("), "{hull}"); + assert!(hull.contains("break"), "{hull}"); + let checked = check_program_with_db(db, &emitted.program); + assert!( + !checked.iter().any(|diagnostic| { + matches!(diagnostic.kind, CheckDiagnosticKind::ExpectedBool { .. }) + }), + "{checked:?}" + ); +} + +#[test] +fn logical_operators_emit_short_circuit_conditionals() { + let (db, output) = specialize_src( + "logical_short_circuit", + r#" +function rhs(value: bool) returns (bool) { + return value; +} + +function main(lhs: bool, rhsValue: bool) returns (bool) { + return (lhs && rhs(rhsValue)) || rhs(rhsValue); +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new()); + assert_eq!(check_program_with_db(db, &emitted.program), Vec::new()); + + let hull = pretty_program(db, &emitted.program); + assert!( + hull.matches("if<").count() >= 2, + "logical operators must lower to conditional Hull:\n{hull}" + ); + assert!( + !hull.contains("and(lhs,") && !hull.contains("or(lhs,"), + "logical operators must not become eager calls:\n{hull}" + ); +} + +#[test] +fn exponentiation_resolves_through_the_pow_trait() { + let hull = pretty_src_hull_with_std( + "pow_operator", + r#" +import std; + +contract C { + function main() public returns (word) { + return 2 ** 3; + } +} +"#, + ); + assert!( + hull.contains("8") || hull.contains("exp("), + "exponentiation did not reach the Pow implementation:\n{hull}" + ); +} + +#[test] +fn word_storage_fixture_reaches_word_slot_ops() { + let repo = repo_root(); + let fixture = + repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/spec/120basicCounter.solc"); + let (db, output) = specialize_fixture(&fixture); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + let hull = pretty_program(db, &emitted.program); + assert!(hull.contains("sload") || hull.contains("sstore"), "{hull}"); + assert!( + !emitted.diagnostics.iter().any(|diagnostic| { + matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedMonoConstruct { construct } + if construct == "field access" || construct == "index access" + ) + }), + "{:?}", + emitted.diagnostics + ); +} + +#[test] +fn value_type_storage_field_uses_underlying_word_slot_ops() { + let hull = pretty_src_hull( + "value_type_storage", + r#" +type Wad is word; + +contract Vault { + value: Wad; + + function set(input: word) { + value = input as Wad; + } + + function main() public returns (word) { + set(1); + return value as word; + } +} +"#, + ); + let main = hull_function(&hull, "_main_"); + assert!(hull.contains("sstore(0,"), "{main}\n{hull}"); + assert!(main.contains("return sload(0)"), "{main}\n{hull}"); +} + +#[test] +fn non_word_value_type_storage_fails_closed_in_the_emitter() { + let (db, output) = specialize_src( + "value_type_bool_storage", + r#" +type Flag is bool; + +contract Vault { + value: Flag; + + function main() public returns (bool) { + return value; + } +} +"#, + ); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("Flag") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("sload("), "{hull}"); + assert!(!hull.contains("sstore("), "{hull}"); +} + +#[test] +fn fixed_array_storage_fails_closed_in_the_emitter() { + let (db, output) = specialize_src( + "fixed_array_storage", + r#" +contract Vault { + alias Values = word[3]; + values: Values; + + function main() public returns (word) { + return 0; + } +} +"#, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("word[3]") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("sload("), "{hull}"); + assert!(!hull.contains("sstore("), "{hull}"); +} + +#[test] +fn mapping_with_fixed_array_value_fails_closed_before_storage_fast_path() { + let (db, output) = specialize_src_with_std( + "mapping_fixed_array_storage", + r#" +import std; + +contract Vault { + values: mapping(word => word[3]); + + function main() public returns (word) { + return 0; + } +} +"#, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("word[3]") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("sload("), "{hull}"); + assert!(!hull.contains("sstore("), "{hull}"); + assert!(!hull.contains("__storage_mapping_value"), "{hull}"); +} + +#[test] +fn generic_contract_alias_with_fixed_array_fails_closed_before_mapping_fast_path() { + let (db, output) = specialize_src_with_std( + "generic_contract_alias_fixed_array_storage", + r#" +import std; + +contract C { + alias Values = word[3]; + values: mapping(word => Values); + + function main() public returns (word) { + return 0; + } +} +"#, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("word[3]") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("__storage_mapping_value"), "{hull}"); +} + +#[test] +fn mapping_with_adt_hidden_fixed_array_fails_closed_before_storage_fast_path() { + let (db, output) = specialize_src_with_std( + "mapping_adt_fixed_array_storage", + r#" +import std; + +struct Box { + values: word[3]; +} + +contract Vault { + values: mapping(word => Box); + + function main() public returns (word) { + return 0; + } +} +"#, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("Box") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("__storage_mapping_value"), "{hull}"); +} + +#[test] +fn direct_adt_hidden_fixed_array_is_rejected_before_layout_fallback() { + let (db, output) = specialize_src( + "direct_adt_fixed_array_storage", + r#" +struct Box { + values: word[3]; +} + +contract Vault { + boxed: Box; + + function main() public returns (word) { + return 0; + } +} +"#, + ); + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("Box") + )), + "{:?}", + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("sload("), "{hull}"); + assert!(!hull.contains("sstore("), "{hull}"); +} + +#[test] +fn recursive_generic_adt_checks_type_arguments_before_cycle_cutoff() { + let (db, output) = specialize_src_with_std( + "recursive_generic_adt_fixed_array_storage", + r#" +import std; + +struct Loop { + next: Loop; +} + +contract Vault { + values: mapping(word => Loop); + + function main() public returns (word) { + return 0; + } +} +"#, + ); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedType { ty } if ty.contains("Loop") + )), + "specialize={:?}, emit={:?}", + output.diagnostics, + emitted.diagnostics + ); + let hull = pretty_program(db, &emitted.program); + assert!(!hull.contains("__storage_mapping_value"), "{hull}"); +} + +#[test] +fn single_constructor_matches_project_payloads_from_scrutinee() { + assert_fixture_emits_and_checks("cases/encoder1.solc"); + assert_fixture_has_no_unbound_alt("cases/mptc-multi-instance.solc"); +} + +#[test] +fn decision_tree_match_lowering_preserves_priority_nested_and_multi_scrutinee_cases() { + for fixture in [ + "spec/033join.solc", + "spec/038food0.solc", + "cases/Option.solc", + "cases/option2.solc", + "cases/dot-pattern-nested-constructor.solc", + "cases/Logic.solc", + "cases/Ackermann.solc", + "cases/false-redundant-warning.solc", + "cases/super-class.solc", + ] { + assert_fixture_emits_without_match_lowering_regressions(fixture); + } +} + +#[test] +fn decision_tree_shape_preserves_specific_constructors_before_wildcard_defaults() { + let dwarves = pretty_src_hull( + "dwarves_runtime_shape", + r#" +contract Dwarves { + enum Dwarf { Doc, Grumpy, Sleepy, Bashful, Happy, Sneezy, Dopey } + + function fromEnum(c: Dwarf) public returns (word) { + assembly { mstore(0, 0) } + match (c) { case Dwarf.Doc { return 1; } case Dwarf.Grumpy { return 2; } case Dwarf.Sleepy { return 3; } case Dwarf.Bashful { return 4; } case Dwarf.Happy { return 5; } default { return 0; } } + } + + function main() returns (word) { return fromEnum(Dwarf.Happy); } +} +"#, + ); + assert_contains_in_order( + "037dwarves", + &dwarves, + &[ + "/* Doc */", + "return 1", + "/* Grumpy */", + "return 2", + "/* Sleepy */", + "return 3", + "/* Bashful */", + "return 4", + "/* Happy */", + "return 5", + "return 0", + ], + ); + + let food0_actual = pretty_fixture_hull("spec/038food0.solc"); + assert!( + food0_actual.contains("function 038food0_FoodContract_main"), + "{food0_actual}" + ); + assert!(food0_actual.contains("return 42"), "{food0_actual}"); + + let food0_shape = pretty_src_hull( + "food0_runtime_shape", + r#" +enum Food { Curry, Beans, Other } +enum CFood { Red(Food), Green(Food), Nocolor } + +function fromEnum(x: CFood) returns (word) { + assembly { mstore(0, 0) } + match (x) { case CFood.Red(Food.Curry) { return 1; } case CFood.Green(Food.Beans) { return 42; } default { return 3; } } +} + +contract FoodContract { + function main() returns (word) { return fromEnum(CFood.Green(Food.Beans)); } +} +"#, + ); + assert_contains_in_order( + "food0 runtime shape", + &food0_shape, + &[ + "/* Red */", + "/* Curry */", + "return 1", + "/* Green */", + "/* Beans */", + "return 42", + ], + ); + + let food = pretty_fixture_hull("spec/039food.solc"); + assert!( + food.contains("function 039food_FoodContract_main") && food.contains("return 42"), + "{food}" + ); + + let wildcard_after_ctor = pretty_src_hull( + "wildcard_after_ctor", + r#" +enum Tiny { A, B, C } + +contract C { + function pick(t: Tiny) public returns (word) { + assembly { mstore(0, 0) } + match (t) { case Tiny.B { return 2; } default { return 9; } } + } + + function main() returns (word) { return pick(Tiny.B); } +} +"#, + ); + assert_contains_in_order( + "minimal wildcard after constructor", + &wildcard_after_ctor, + &["/* B */", "return 2", "return 9"], + ); +} + +#[test] +fn cited_terminal_yul_fixtures_do_not_fail_missing_terminator() { + for fixture in [ + "cases/yul-return.solc", + "cases/undefined.solc", + "cases/copytomem.solc", + ] { + let kinds = check_fixture_kinds(fixture); + assert!( + !kinds + .iter() + .any(|kind| { matches!(kind, CheckDiagnosticKind::MissingTerminator { .. }) }), + "{fixture}: {kinds:?}" + ); + } +} + +#[test] +fn recursive_adt_layouts_are_cycle_safe() { + for fixture in ["cases/PeanoMatch.solc", "cases/listid.solc"] { + assert_fixture_emits_and_checks(fixture); + } +} + +#[test] +fn unsupported_match_rows_produce_an_explicit_emit_diagnostic() { + let (db, output) = specialize_src( + "string_literal_match", + r#" +function main(s: string) returns (word) { + match (s) { case "a" { return 1; } default { return 2; } } +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + + assert!( + emitted.diagnostics.iter().any(|diagnostic| matches!( + &diagnostic.kind, + EmitDiagnosticKind::UnsupportedMonoConstruct { construct } + if construct.contains("string literal match pattern") + )), + "{:?}", + emitted.diagnostics + ); +} + +#[test] +fn out_of_range_word_literals_wrap_in_hull_exprs_and_patterns() { + const TWO_256: &str = + "115792089237316195423570985008687907853269984665640564039457584007913129639936"; + const TWO_256_PLUS_ONE: &str = + "115792089237316195423570985008687907853269984665640564039457584007913129639937"; + + let hull = pretty_src_hull_with_std( + "word_literal_wrap", + &format!( + r#" +import std; +import std.dispatch; + +contract C {{ + function exact() returns (word) {{ + return {TWO_256}; + }} + + function plus() returns (word) {{ + return {TWO_256_PLUS_ONE}; + }} + + function pick(x: word) returns (word) {{ + match (x) {{ + case {TWO_256} {{ return 10; }} + case {TWO_256_PLUS_ONE} {{ return 11; }} + default {{ return 12; }} + }} + }} + + function main() public returns (word) {{ + let x: word = 0; + assembly {{ x := calldataload(0) }} + return exact() + plus() + pick(x); + }} + +}} +"# + ), + ); + + assert!(!hull.contains(TWO_256), "{hull}"); + assert!(!hull.contains(TWO_256_PLUS_ONE), "{hull}"); + assert!( + hull.contains("Add_add_d") && hull.contains("$word(1,"), + "{hull}" + ); + + let pick = hull_function(&hull, "main_C_pick_"); + assert_contains_in_order( + "wrapped word pattern literals", + pick, + &[ + "match", + "0 ", + "return 10", + "1 ", + "return 11", + "return 12", + ], + ); +} + +#[test] +fn value_equal_word_patterns_share_one_canonical_switch_branch() { + let hull = pretty_src_hull_with_std( + "equal_literal_spellings", + r#" +contract C { + function pick(x: word) returns (word) { + match (x) { case 0x2a { return 111; } case 0042 { return 222; } default { return 333; } } + } + + function main() returns (word) { + let x: word = 0; + assembly { x := calldataload(0) } + return pick(x); + } +} +"#, + ); + + let pick = hull_function(&hull, "main_C_pick_"); + assert_eq!( + pick.lines() + .filter(|line| line.trim_start().starts_with("42 ")) + .count(), + 1, + "{pick}" + ); + assert!(pick.contains("return 111"), "{pick}"); + assert!(!pick.contains("return 222"), "{pick}"); + assert!(pick.contains("return 333"), "{pick}"); +} + +#[test] +fn evaluator_does_not_fold_past_unknown_return() { + let hull = pretty_src_hull_with_std( + "eval_return_unknown_abort", + r#" +import std; +import std.dispatch; + +contract RetUnknown { + function pick(flag: bool, y: word) returns (word) { + match (flag) { case true { return y; } case false { return 5; } } + return 0; + } + + function get(x: word) returns (word) { + return pick(true, x); + } + + function main() public returns (word) { + let x: word = 0; + assembly { x := calldataload(0) } + return get(x); + } + +} +"#, + ); + let get = hull_function(&hull, "main_RetUnknown_get_"); + assert!(get.contains("_pick_"), "{get}\n{hull}"); + assert!(!get.contains("return 0"), "{get}\n{hull}"); +} + +#[test] +fn evaluator_does_not_inline_storage_writing_helpers() { + let mapping_hull = pretty_src_hull_with_std( + "eval_storage_writer_mapping", + r#" +import std; + +contract MappingWriter { + m: mapping(word => word); + + function set(k: word, v: word) returns (word) { + m[k] = v; + return v; + } + + function main() public returns (word) { + let a: word = set(1, 42); + return m[1]; + } +} +"#, + ); + let mapping_main = hull_function(&mapping_hull, "_main_"); + assert!( + mapping_main.contains("_set_"), + "{mapping_main}\n{mapping_hull}" + ); + assert!(mapping_hull.contains("sstore("), "{mapping_hull}"); + assert!( + mapping_main.contains("sload(__solcore_storage_hash2(0, 1))"), + "{mapping_main}\n{mapping_hull}" + ); + + let direct_hull = pretty_src_hull_with_std( + "eval_storage_writer_direct", + r#" +import std; + +contract DirectWriter { + x: word; + + function setv(v: word) returns (word) { + x = v; + return v; + } + + function main() public returns (word) { + let a: word = setv(9); + return x; + } +} +"#, + ); + let direct_main = hull_function(&direct_hull, "_main_"); + assert!( + direct_main.contains("_setv_"), + "{direct_main}\n{direct_hull}" + ); + assert!(direct_hull.contains("sstore(0,"), "{direct_hull}"); + assert!( + direct_main.contains("return sload(0)"), + "{direct_main}\n{direct_hull}" + ); + assert!( + !direct_main.contains("return 9"), + "{direct_main}\n{direct_hull}" + ); +} + +#[test] +fn storage_index_assignment_materializes_slot_before_rhs() { + let hull = pretty_src_hull_with_std( + "storage_index_order", + r#" +import std; + +contract StorageIndexOrder { + counter: word; + m: mapping(word => word); + + function next() returns (word) { + let cur: word = counter; + let res: word; + assembly { + res := add(cur, 1) + } + counter = res; + return res; + } + + function main() public returns (word) { + counter = 0; + m[next()] = next(); + return m[1]; + } +} +"#, + ); + let main = hull_function(&hull, "_main_"); + assert_contains_in_order( + "storage index assignment order", + main, + &[ + "storage_store_storage_index_slot_1 := __solcore_storage_hash2(1, main_StorageIndexOrder_next_", + "storage_store_storage_index_2 := main_StorageIndexOrder_next_", + "sstore(storage_store_storage_index_slot_1, storage_store_storage_index_2)", + ], + ); + + let compound_hull = pretty_src_hull_with_std( + "storage_index_compound", + r#" +import std; + +contract StorageIndexCompound { + counter: word; + m: mapping(word => word); + + function next() returns (word) { + let cur: word = counter; + let res: word; + assembly { + res := add(cur, 1) + } + counter = res; + return res; + } + + function main() public returns (word) { + counter = 0; + m[1] = 10; + m[next()] += next(); + return m[1]; + } +} +"#, + ); + let compound_main = hull_function(&compound_hull, "_main_"); + assert_contains_in_order( + "compound storage index assignment order", + compound_main, + &[ + "storage_store_storage_index_slot_3 := __solcore_storage_hash2(1, main_StorageIndexCompound_next_", + "storage_store_storage_index_4 := Add_add_", + "(sload(storage_store_storage_index_slot_3), main_StorageIndexCompound_next_", + "sstore(storage_store_storage_index_slot_3, storage_store_storage_index_4)", + ], + ); + assert_eq!( + compound_main + .matches("main_StorageIndexCompound_next_") + .count(), + 2, + "{compound_main}" + ); +} + +#[test] +fn evaluator_invalidates_storage_bindings_after_residual_calls() { + let hull = pretty_src_hull( + "eval_stale_storage_call", + r#" +contract StaleCall { + x: word; + + function setx() returns () { + x = 8; + } + + function main() public returns (word) { + x = 7; + setx(); + return x; + } +} +"#, + ); + let main = hull_function(&hull, "_main_"); + assert_contains_in_order( + "stale storage call main", + main, + &["sstore(0,", "_setx_", "return sload(0)"], + ); + assert!(!main.contains("return 7"), "{main}\n{hull}"); +} + +#[test] +fn audit_p0_match_scrutinees_are_materialized_exactly_once_even_for_default_bindings() { + for (name, arms) in [ + ( + "match_call_default_binding", + "case 0 { return 0; } case n { return n; }", + ), + ("match_call_wildcard", "default { return 7; }"), + ] { + let hull = pretty_src_hull( + name, + &format!( + r#" +function read(x: word) returns (word) {{ + let value: word; + assembly {{ value := sload(x) }} + return value; +}} + +contract C {{ + function main() public returns (word) {{ + match (read(0)) {{ {arms} }} + }} +}} +"# + ), + ); + let main = hull_function(&hull, "_main_"); + assert_eq!(main.matches("_read_").count(), 1, "{name}: {main}\n{hull}"); + assert!(main.contains("$match_scrutinee"), "{name}: {main}\n{hull}"); + } +} + +#[test] +fn audit_p0_shadowing_let_materializes_its_initializer_before_declaration() { + let hull = pretty_src_hull( + "shadowing_let_initializer", + r#" +contract C { + balance: word; + + function main() public returns (word) { + let balance: word = balance; + return balance; + } +} +"#, + ); + let main = hull_function(&hull, "_main_"); + assert_contains_in_order( + "shadowing let initializer", + main, + &[ + "$let_init", + "sload(0)", + "let balance", + "balance := $let_init", + "return balance", + ], + ); +} + +#[test] +fn audit_p0_for_initializer_let_remains_visible_after_the_loop() { + let hull = pretty_src_hull( + "for_initializer_scope", + r#" +contract C { + i: word; + + function main() public returns (word) { + for (let i: word; false; ) {} + return i; + } +} +"#, + ); + let main = hull_function(&hull, "_main_"); + assert_contains_in_order("for initializer scope", main, &["let i", "for", "return i"]); + assert!(!main.contains("return sload"), "{main}\n{hull}"); +} + +#[test] +fn audit_p0_if_branch_let_is_hoisted_and_remains_a_local() { + let hull = pretty_src_hull_with_std( + "if_branch_let_scope", + r#" +import std; + +contract C { + x: word; + + function f(flag: bool) returns (word) { + if (flag && true) { + let x: word = 7; + } + return x; + } + + function main() public returns (word) { return f(tobool(x)); } +} +"#, + ); + let main = hull_function(&hull, "_f_"); + assert_contains_in_order( + "if branch let hoisting", + main, + &[ + "let $if_local", + "match", + "$if_local", + ":= 7", + "return $if_local", + ], + ); + assert!(!main.contains("return sload(0)"), "{main}\n{hull}"); + + assert!( + !hull.contains("std_and_"), + "logical conjunction must not lower to an eager std function:\n{hull}" + ); +} + +#[test] +fn evaluator_invalidates_residual_assembly_branch_assignments() { + let if_hull = pretty_src_hull_with_std( + "eval_if_asm_assignment", + r#" +import std; +import std.dispatch; + +contract IfAsm { + function f(b: bool) returns (word) { + let x: word = 1; + if (b) { + assembly { x := 5 } + } + return x; + } + + function main() public returns (word) { + let raw: word = 0; + assembly { raw := calldataload(0) } + let b: bool = tobool(raw); + return f(b); + } + +} +"#, + ); + let f = hull_function(&if_hull, "main_IfAsm_f_"); + assert!(f.contains("x := 5"), "{f}\n{if_hull}"); + assert!(f.contains("return x"), "{f}\n{if_hull}"); + assert!(!f.contains("return 1"), "{f}\n{if_hull}"); + + let match_hull = pretty_src_hull_with_std( + "eval_match_asm_assignment", + r#" +import std; +import std.dispatch; + +contract MatchAsm { + function g(b: bool) returns (word) { + let x: word = 1; + match (b) { case true { assembly { x := 5 } } case false {} } + return x; + } + + function main() public returns (word) { + let raw: word = 0; + assembly { raw := calldataload(0) } + let b: bool = tobool(raw); + return g(b); + } + +} +"#, + ); + let g = hull_function(&match_hull, "main_MatchAsm_g_"); + assert!(g.contains("x := 5"), "{g}\n{match_hull}"); + assert!(g.contains("return x"), "{g}\n{match_hull}"); + assert!(!g.contains("return 1"), "{g}\n{match_hull}"); +} + +#[test] +fn cited_nested_layout_fixtures_check_cleanly() { + for fixture in [ + "spec/032simplejoin.solc", + "spec/034cojoin.solc", + "spec/043fstsnd.solc", + ] { + let kinds = check_fixture_kinds(fixture); + assert!(kinds.is_empty(), "{fixture}: {kinds:?}"); + } +} + +fn try_check_fixture_kinds(fixture: &str) -> Result, String> { + let repo = repo_root(); + let fixture_path = repo + .join("crates/parser/tests/fixtures/corpus/ok/test/examples") + .join(fixture); + let (db, output) = specialize_fixture(&fixture_path); + if !output.diagnostics.is_empty() { + return Err(format!("specialize: {:?}", output.diagnostics)); + } + let emitted = emit_module(db, &output.module, EmitOptions::default()); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + if !non_dispatch.is_empty() { + return Err(format!("emit: {non_dispatch:?}")); + } + Ok(check_program_with_db(db, &emitted.program) + .into_iter() + .map(|diagnostic| diagnostic.kind) + .collect()) +} + +fn check_fixture_kinds(fixture: &str) -> Vec { + match try_check_fixture_kinds(fixture) { + Ok(kinds) => kinds, + Err(stage) => panic!("{fixture}: {stage}"), + } +} + +#[test] +fn mapping_field_in_value_position_lowers_to_unimplemented_trap() { + // The reference compiles whole-mapping reads/stores via the + // `mapping(k => v) storage: CanStore` impl, whose load/store are + // `unimplemented()` runtime traps. This must not escape as an internal + // hull-check error (previously: UndefinedVariable { name: "bal" }). + let read_src = r#" +enum mapping { mapping(word) } + +contract C { + bal: mapping(word => word); + + function main() public returns (word) { + let b = bal; + return 7; + } +} +"#; + let store_src = r#" +enum mapping { mapping(word) } + +contract C { + bal: mapping(word => word); + + function main() public returns (word) { + bal = bal; + return 7; + } +} +"#; + for (name, src) in [ + ("mapping_value_read", read_src), + ("mapping_value_store", store_src), + ] { + let (db, output) = specialize_src(name, src); + assert_eq!(output.diagnostics, Vec::new(), "specialize for {name}"); + let emitted = emit_module(db, &output.module, EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new(), "emit for {name}"); + assert_eq!( + check_program_with_db(db, &emitted.program), + Vec::new(), + "check for {name}" + ); + let hull = pretty_program(db, &emitted.program); + assert!( + hull.contains("__solcore_storage_mapping_value"), + "{name}: {hull}" + ); + assert!(hull.contains("0x6e128399"), "{name}: {hull}"); + } +} + +fn specialize_src(name: &str, src: &str) -> (&'static TestDb, SpecializeOutput<'static>) { + let db = Box::leak(Box::new(TestDb::default())); + let module = parse_module(db, name, src); + let output = specialize_module(db, module, SpecializeOptions::default()); + (db, output) +} + +fn parse_module<'db>(db: &'db TestDb, name: &str, src: &str) -> Module<'db> { + let url = format!("memory:///{name}.solc").parse().expect("valid URL"); + let file = SourceFile::new(db, url, Some(src.to_owned())); + parse_file_to_hir(db, file).module(db) +} + +/// Specializes an in-memory source with the standard library on the module +/// path. Unlike `specialize_src`, this mirrors the real driver: `import std` +/// and its instances (e.g. `word:Int`) resolve, so integer literals are typed +/// by their use rather than by eager defaulting. +fn specialize_src_with_std(name: &str, src: &str) -> (&'static TestDb, SpecializeOutput<'static>) { + let main_root = repo_root().join("target/hull-smoke-tmp").join(name); + fs::create_dir_all(&main_root).expect("create temp main root"); + let path = main_root.join("main.solc"); + fs::write(&path, src).expect("write temp source"); + specialize_fixture(&path) +} + +fn specialize_fixture(path: &Path) -> (&'static TestDb, SpecializeOutput<'static>) { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = path.parent().expect("fixture parent").to_path_buf(); + let repo = repo_root(); + let std_root = repo.join("crates/parser/tests/fixtures/corpus/ok/std"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + db, + [main_root.as_path(), std_root.as_path()], + )); + let source = fs::read_to_string(path).expect("fixture source"); + let key = + module_key_for_path(LibraryId::Main, &main_root, path).expect("fixture under main root"); + let file = SourceFile::new( + db, + url::Url::from_file_path(path).expect("file URL"), + Some(source), + ); + db.insert_module_file(key.clone(), file); + let unresolved = load_reachable_modules(db, key); + assert!(unresolved.is_empty(), "{unresolved:?}"); + let module = parse_file_to_hir(db, file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + (db, output) +} + +fn module_fs_snapshot_for_roots<'a>( + db: &TestDb, + roots: impl IntoIterator, +) -> ModuleFsSnapshot { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +fn load_reachable_modules(db: &mut TestDb, entry: ModuleKey) -> Vec { + let mut queue = VecDeque::from([entry]); + let mut visited = FxHashSet::default(); + let mut unresolved = Vec::new(); + + while let Some(key) = queue.pop_front() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = db.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map( + |path| match resolve_module_path_candidate(&*db, module, &path) { + Ok(resolved) => Some((resolved.module.key(&*db), resolved.file_path)), + Err(_) => { + unresolved.push(format!( + "{} imports `{}`", + module.display(&*db), + module_path_display(&*db, &path) + )); + None + } + }, + ) + .collect::>() + }; + for (target_key, file_path) in targets { + if !db.module_files.contains_key(&target_key) { + match fs::read_to_string(&file_path) { + Ok(source) => { + let file = SourceFile::new( + db, + url::Url::from_file_path(&file_path).expect("file URL"), + Some(source), + ); + db.insert_module_file(target_key.clone(), file); + } + Err(err) => unresolved.push(format!("{}: {err}", file_path.display())), + } + } + queue.push_back(target_key); + } + } + unresolved +} + +fn assert_fixture_emits_and_checks(relative: &str) { + let fixture = repo_root() + .join("crates/parser/tests/fixtures/corpus/ok/test/examples") + .join(relative); + let (db, output) = specialize_fixture(&fixture); + assert_eq!( + output.diagnostics, + Vec::new(), + "specialize diagnostics for {relative:?}" + ); + let emitted = emit_module( + db, + &output.module, + EmitOptions { + emit_dispatcher_comments: false, + }, + ); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + assert_eq!( + non_dispatch, + Vec::<&EmitDiagnostic>::new(), + "emit diagnostics for {relative:?}" + ); + assert_eq!( + check_program_with_db(db, &emitted.program), + Vec::new(), + "check diagnostics for {relative:?}" + ); +} + +#[test] +fn overloaded_binary_operators_emit_instance_results() { + let custom_uint = + pretty_src_hull_with_std("operator-custom-uint-add", OPERATOR_CUSTOM_UINT_ADD); + assert!( + custom_uint.contains("42"), + "custom uint Add impl was not reflected in Hull:\n{custom_uint}" + ); + + let meters = pretty_src_hull_with_std("operator-meters-add", OPERATOR_METERS_ADD); + assert!( + meters.contains("3"), + "meters Add impl did not emit the expected result:\n{meters}" + ); + + let meters_ord = pretty_src_hull_with_std("operator-meters-ord", OPERATOR_METERS_ORD); + assert!( + meters_ord.contains("42"), + "meters Ord impl did not emit the expected result:\n{meters_ord}" + ); + + let word = pretty_src_hull_with_std("operator-word-add", OPERATOR_WORD_ADD); + assert!( + word.contains("3"), + "word Add impl changed observable Hull result:\n{word}" + ); +} + +const OPERATOR_CUSTOM_UINT_ADD: &str = r#" +import std; + +enum uint { u(word) } + +impl Add { + function add(x: uint, y: uint) returns (uint) { + return uint.u(42); + } +} + +function unwrap(x: uint) returns (word) { + match (x) { case uint.u(w) { return w; } } +} + +contract C { + function main() public returns (word) { + let a: uint = uint.u(1); + let b: uint = uint.u(2); + let c: uint = a + b; + return unwrap(c); + } +} +"#; + +const OPERATOR_METERS_ADD: &str = r#" +import std; + +enum meters { meters(word) } + +impl Add { + function add(x: meters, y: meters) returns (meters) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return meters.meters(addWord(xw, yw)); } } + } +} + +function unwrap(x: meters) returns (word) { + match (x) { case meters.meters(w) { return w; } } +} + +contract C { + function main() public returns (word) { + let a: meters = meters.meters(1); + let b: meters = meters.meters(2); + let c: meters = a + b; + return unwrap(c); + } +} +"#; + +const OPERATOR_METERS_ORD: &str = r#" +import std; + +enum meters { meters(word) } + +impl Eq { + function eq(x: meters, y: meters) returns (bool) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return eqWord(xw, yw); } } + } +} + +impl Ord { + function gt(x: meters, y: meters) returns (bool) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return gtWord(xw, yw); } } + } +} + +contract C { + function main() public returns (word) { + let a: meters = meters.meters(1); + let b: meters = meters.meters(2); + if (a < b) { + return 42; + } else { + return 0; + } + } +} +"#; + +const OPERATOR_WORD_ADD: &str = r#" +import std; + +contract C { + function main() public returns (word) { + return 1 + 2; + } +} +"#; + +fn pretty_fixture_hull(relative: &str) -> String { + let fixture = repo_root() + .join("crates/parser/tests/fixtures/corpus/ok/test/examples") + .join(relative); + let (db, output) = specialize_fixture(&fixture); + pretty_output_hull(db, output, relative) +} + +fn pretty_src_hull(name: &str, src: &str) -> String { + let (db, output) = specialize_src(name, src); + pretty_output_hull(db, output, name) +} + +fn pretty_src_hull_with_std(name: &str, src: &str) -> String { + let (db, output) = specialize_src_with_std(name, src); + pretty_output_hull(db, output, name) +} + +fn pretty_output_hull( + db: &'static TestDb, + output: SpecializeOutput<'static>, + label: &str, +) -> String { + assert_eq!( + output.diagnostics, + Vec::new(), + "specialize diagnostics for {label:?}" + ); + let emitted = emit_module( + db, + &output.module, + EmitOptions { + emit_dispatcher_comments: false, + }, + ); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + assert_eq!( + non_dispatch, + Vec::<&EmitDiagnostic>::new(), + "emit diagnostics for {label:?}" + ); + assert_eq!( + check_program_with_db(db, &emitted.program), + Vec::new(), + "check diagnostics for {label:?}" + ); + pretty_program(db, &emitted.program) +} + +fn assert_contains_in_order(label: &str, haystack: &str, needles: &[&str]) { + let mut offset = 0; + for needle in needles { + let Some(found) = haystack[offset..].find(needle) else { + panic!("{label}: missing ordered snippet {needle:?}\n{haystack}"); + }; + offset += found + needle.len(); + } +} + +fn hull_function<'a>(hull: &'a str, name_fragment: &str) -> &'a str { + let mut search_from = 0; + while let Some(relative_start) = hull[search_from..].find("function ") { + let start = search_from + relative_start; + let header_line_end = hull[start..] + .find('\n') + .map(|offset| start + offset) + .unwrap_or(hull.len()); + let header_end = hull[start..header_line_end] + .rfind('{') + .map(|offset| start + offset) + .expect("function header has body"); + let header = &hull[start..header_end]; + let body_start = header_end + 1; + let mut depth = 1usize; + for (offset, ch) in hull[body_start..].char_indices() { + match ch { + '{' => depth += 1, + '}' => { + depth -= 1; + if depth == 0 { + let end = body_start + offset + ch.len_utf8(); + if header.contains(name_fragment) { + return &hull[start..end]; + } + search_from = end; + break; + } + } + _ => {} + } + } + } + panic!("missing function containing {name_fragment:?}\n{hull}"); +} + +fn assert_fixture_emits_without_match_lowering_regressions(relative: &str) { + let fixture = repo_root() + .join("crates/parser/tests/fixtures/corpus/ok/test/examples") + .join(relative); + let (db, output) = specialize_fixture(&fixture); + assert_eq!( + output.diagnostics, + Vec::new(), + "specialize diagnostics for {relative:?}" + ); + let emitted = emit_module( + db, + &output.module, + EmitOptions { + emit_dispatcher_comments: false, + }, + ); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + assert_eq!( + non_dispatch, + Vec::<&EmitDiagnostic>::new(), + "emit diagnostics for {relative:?}" + ); + + let checked = check_program_with_db(db, &emitted.program); + assert!( + !checked.iter().any(|diagnostic| matches!( + &diagnostic.kind, + CheckDiagnosticKind::UndefinedVariable { name } if name.starts_with("$alt") + )), + "unbound alt diagnostic for {relative:?}: {checked:?}" + ); + let unexpected: Vec<_> = checked + .iter() + .filter(|diagnostic| { + !matches!( + diagnostic.kind, + CheckDiagnosticKind::ExprAnnotationMismatch { .. } + | CheckDiagnosticKind::TypeMismatch { .. } + ) + }) + .collect(); + assert!( + unexpected.is_empty(), + "unexpected check diagnostics for {relative:?}: {unexpected:?}" + ); +} + +fn assert_fixture_has_no_unbound_alt(relative: &str) { + let fixture = repo_root() + .join("crates/parser/tests/fixtures/corpus/ok/test/examples") + .join(relative); + let (db, output) = specialize_fixture(&fixture); + assert_eq!( + output.diagnostics, + Vec::new(), + "specialize diagnostics for {relative:?}" + ); + let emitted = emit_module( + db, + &output.module, + EmitOptions { + emit_dispatcher_comments: false, + }, + ); + let non_dispatch: Vec<_> = emitted + .diagnostics + .iter() + .filter(|d| !matches!(d.kind, EmitDiagnosticKind::UnsupportedDispatchEntry { .. })) + .collect(); + assert_eq!( + non_dispatch, + Vec::<&EmitDiagnostic>::new(), + "emit diagnostics for {relative:?}" + ); + let checked = check_program_with_db(db, &emitted.program); + assert!( + !checked.iter().any(|diagnostic| matches!( + &diagnostic.kind, + CheckDiagnosticKind::UndefinedVariable { name } if name.starts_with("$alt") + )), + "unbound alt diagnostic for {relative:?}: {checked:?}" + ); +} + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("crate is under repo/crates/hull") + .to_path_buf() +} diff --git a/crates/hull/tests/snapshots.rs b/crates/hull/tests/snapshots.rs new file mode 100644 index 00000000..75860386 --- /dev/null +++ b/crates/hull/tests/snapshots.rs @@ -0,0 +1,786 @@ +// migrate-syntax: keep-rust-file + +use hir::{ + anchor::DefLocationTable, + ast::{ + Ident, + function::{YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind}, + }, + diag::Offset, + input::SourceFile, + span::{AnchorId, Span, SpannedElem}, +}; +use parser::parse_file_to_hir; +use solcore_hull::{ + Alt, Arg, CheckDiagnosticKind, CodeBlock, Con, Expr, Function, Object, Pat, PatKind, Program, + Stmt, StmtKind, Ty, check_program_with_db, pretty_program, +}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +fn test_span<'db>(db: &'db TestDb) -> Span<'db> { + let file = SourceFile::new( + db, + "memory:///hull_snapshots.solc".parse().expect("valid URL"), + Some(String::new()), + ); + Span::new(AnchorId::root(db, file), Offset::new(0), Offset::new(0)) +} + +#[test] +fn identity_function_snapshot() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "id".into(), + args: vec![Arg { + span: sp, + name: "x".into(), + ty: word.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "x", word)), + }], + }], + objects: Vec::new(), + }; + + assert_eq!(check_program_with_db(&db, &program), Vec::new()); + assert_eq!( + pretty_program(&db, &program), + "function id (x : word) -> word {\n return x\n}\n" + ); +} + +#[test] +fn maybe_option_snapshot() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let option = Ty::named(sp, "Option", Ty::sum(sp, Ty::unit(sp), Ty::word(sp))); + let alt_ty = Ty::word(sp); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "maybe$Word".into(), + args: vec![ + Arg { + span: sp, + name: "n".into(), + ty: word.clone(), + }, + Arg { + span: sp, + name: "o".into(), + ty: option.clone(), + }, + ], + ret: word.clone(), + body: vec![Stmt { + span: sp, + kind: StmtKind::Match { + target: option.clone(), + scrutinee: Expr::var(sp, "o", option), + alts: vec![ + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inl), + }, + binder: "$alt".into(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Comment("None".to_owned()), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "n", word.clone())), + }, + ], + }, + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inr), + }, + binder: "$alt".into(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Comment("Some".to_owned()), + }, + Stmt { + span: sp, + kind: StmtKind::Let { + name: "var_1".into(), + ty: alt_ty.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Assign { + lhs: Expr::var(sp, "var_1", alt_ty.clone()), + rhs: Expr::var(sp, "$alt", alt_ty.clone()), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "var_1", word.clone())), + }, + ], + }, + ], + }, + }], + }], + objects: Vec::new(), + }; + + assert_eq!(check_program_with_db(&db, &program), Vec::new()); + assert_eq!( + pretty_program(&db, &program), + concat!( + // migrate-syntax: keep-rust-concat + "function maybe$Word (n : word, o : Option{(unit + word)}) -> word {\n", + " match o with {\n", + " inl $alt => {\n", + " /* None */\n", + " return n\n", + " }\n", + " inr $alt => {\n", + " /* Some */\n", + " let var_1 : word\n", + " var_1 := $alt\n", + " return var_1\n", + " }\n", + " }\n", + "}\n" + ) + ); +} + +#[test] +fn color_enum_snapshot() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let color = Ty::named( + sp, + "Color", + Ty::sum(sp, Ty::unit(sp), Ty::sum(sp, Ty::unit(sp), Ty::unit(sp))), + ); + let tail = Ty::sum(sp, Ty::unit(sp), Ty::unit(sp)); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "fromEnum".into(), + args: vec![Arg { + span: sp, + name: "c".into(), + ty: color.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span: sp, + kind: StmtKind::Match { + target: color.clone(), + scrutinee: Expr::var(sp, "c", color), + alts: vec![ + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inl), + }, + binder: "$alt".into(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Comment("Red".to_owned()), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::word(sp, "0")), + }, + ], + }, + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inr), + }, + binder: "$alt".into(), + body: vec![Stmt { + span: sp, + kind: StmtKind::Match { + target: tail.clone(), + scrutinee: Expr::var(sp, "$alt", tail.clone()), + alts: vec![ + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inl), + }, + binder: "$alt".into(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Comment("Green".to_owned()), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::word(sp, "1")), + }, + ], + }, + Alt { + span: sp, + pat: Pat { + span: sp, + kind: PatKind::Con(Con::Inr), + }, + binder: "$alt".into(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Comment("Blue".to_owned()), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::word(sp, "2")), + }, + ], + }, + ], + }, + }], + }, + ], + }, + }], + }], + objects: Vec::new(), + }; + + assert_eq!(check_program_with_db(&db, &program), Vec::new()); + assert_eq!( + pretty_program(&db, &program), + concat!( + // migrate-syntax: keep-rust-concat + "function fromEnum (c : Color{(unit + (unit + unit))}) -> word {\n", + " match c with {\n", + " inl $alt => {\n", + " /* Red */\n", + " return 0\n", + " }\n", + " inr $alt => {\n", + " match<(unit + unit)> $alt with {\n", + " inl $alt => {\n", + " /* Green */\n", + " return 1\n", + " }\n", + " inr $alt => {\n", + " /* Blue */\n", + " return 2\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}\n" + ) + ); +} + +#[test] +fn add1_contract_object_snapshot() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let res = spanned_ident(&db, sp, "res"); + let add = spanned_ident(&db, sp, "add"); + let assembly = YulStmt { + span: sp, + kind: YulStmtKind::Assign { + names: vec![res], + value: YulExpr { + span: sp, + kind: YulExprKind::Call { + name: add, + args: vec![ + YulExpr { + span: sp, + kind: YulExprKind::Lit(YulLitKind::Number("40".to_owned())), + }, + YulExpr { + span: sp, + kind: YulExprKind::Lit(YulLitKind::Number("2".to_owned())), + }, + ], + }, + }, + }, + }; + let main = Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: word.clone(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Let { + name: "res".into(), + ty: word.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Assembly(vec![assembly]), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "res", word)), + }, + ], + }; + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: Vec::new(), + objects: vec![Object { + span: sp, + name: "Add1".into(), + code: CodeBlock { + span: sp, + stmts: vec![Stmt { + span: sp, + kind: StmtKind::Comment("deployment code".to_owned()), + }], + functions: Vec::new(), + }, + inners: vec![Object { + span: sp, + name: "Add1_deployed".into(), + code: CodeBlock { + span: sp, + stmts: Vec::new(), + functions: vec![main], + }, + inners: Vec::new(), + }], + }], + }; + + assert_eq!(check_program_with_db(&db, &program), Vec::new()); + assert_eq!( + pretty_program(&db, &program), + concat!( + // migrate-syntax: keep-rust-concat + "object \"Add1\" {\n", + " code {\n", + " /* deployment code */\n", + " }\n", + " object \"Add1_deployed\" {\n", + " code {\n", + " function main () -> word {\n", + " let res : word\n", + " assembly {\n", + " res := add(40, 2)\n", + " }\n", + " return res\n", + " }\n", + " }\n", + " }\n", + "}\n" + ) + ); +} + +#[test] +fn for_condition_must_be_bool_like() { + let db = TestDb::default(); + let sp = test_span(&db); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: Ty::unit(sp), + body: vec![Stmt { + span: sp, + kind: StmtKind::For { + init: Vec::new(), + cond: Expr::word(sp, "0"), + post: Vec::new(), + body: Vec::new(), + }, + }], + }], + objects: Vec::new(), + }; + + let diagnostics = check_program_with_db(&db, &program); + assert!( + diagnostics + .iter() + .any(|diagnostic| matches!(diagnostic.kind, CheckDiagnosticKind::ExpectedBool { .. })), + "{diagnostics:?}" + ); +} + +#[test] +fn assembly_checker_rejects_bad_assignments_and_usr_call_arity() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let bool_sum = Ty::sum(sp, Ty::unit(sp), Ty::unit(sp)); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![ + Function { + span: sp, + name: "id".into(), + args: vec![Arg { + span: sp, + name: "x".into(), + ty: word.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "x", word.clone())), + }], + }, + Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: word.clone(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Let { + name: "x".into(), + ty: word.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Let { + name: "b".into(), + ty: bool_sum, + }, + }, + Stmt { + span: sp, + kind: StmtKind::Assembly(vec![ + yul_assign( + &db, + sp, + &["x"], + yul_call( + &db, + sp, + "mstore", + vec![yul_num(sp, "1"), yul_num(sp, "1")], + ), + ), + yul_assign( + &db, + sp, + &["b"], + yul_call(&db, sp, "add", vec![yul_num(sp, "1"), yul_num(sp, "1")]), + ), + yul_assign( + &db, + sp, + &["x"], + yul_call( + &db, + sp, + "usr$id", + vec![yul_num(sp, "1"), yul_num(sp, "2")], + ), + ), + ]), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "x", word)), + }, + ], + }, + ], + objects: Vec::new(), + }; + + let diagnostics = check_program_with_db(&db, &program); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + CheckDiagnosticKind::AssemblyReturnCountMismatch { + expected: 1, + actual: 0, + .. + } + )), + "{diagnostics:?}" + ); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + CheckDiagnosticKind::AssemblyExpectedWordAssignment { ref name, .. } if name == "b" + )), + "{diagnostics:?}" + ); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + CheckDiagnosticKind::ArityMismatch { + ref name, + expected: 1, + actual: 2, + } if name == "usr$id" + )), + "{diagnostics:?}" + ); +} + +#[test] +fn assembly_checker_rejects_multi_return_arity_mismatch() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: word.clone(), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Let { + name: "x".into(), + ty: word.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Let { + name: "y".into(), + ty: word.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Let { + name: "z".into(), + ty: word.clone(), + }, + }, + Stmt { + span: sp, + kind: StmtKind::Assembly(vec![ + YulStmt { + span: sp, + kind: YulStmtKind::FunctionDef { + name: spanned_ident(&db, sp, "pair"), + params: Vec::new(), + rets: vec![ + spanned_ident(&db, sp, "a"), + spanned_ident(&db, sp, "b"), + ], + body: Vec::new(), + }, + }, + yul_assign( + &db, + sp, + &["x", "y", "z"], + yul_call(&db, sp, "pair", Vec::new()), + ), + ]), + }, + Stmt { + span: sp, + kind: StmtKind::Return(Expr::var(sp, "x", word)), + }, + ], + }], + objects: Vec::new(), + }; + + let diagnostics = check_program_with_db(&db, &program); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + CheckDiagnosticKind::AssemblyReturnCountMismatch { + expected: 3, + actual: 2, + .. + } + )), + "{diagnostics:?}" + ); +} + +#[test] +fn terminal_yul_return_satisfies_terminator_analysis() { + let db = TestDb::default(); + let sp = test_span(&db); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: Ty::word(sp), + body: vec![Stmt { + span: sp, + kind: StmtKind::Assembly(vec![yul_expr_stmt( + &db, + sp, + yul_call(&db, sp, "return", vec![yul_num(sp, "0"), yul_num(sp, "0")]), + )]), + }], + }], + objects: Vec::new(), + }; + + assert_eq!(check_program_with_db(&db, &program), Vec::new()); +} + +#[test] +fn expression_type_annotations_must_match_inferred_type() { + let db = TestDb::default(); + let sp = test_span(&db); + let word = Ty::word(sp); + let program = Program { + span: sp, + entry_points: Vec::new(), + functions: vec![Function { + span: sp, + name: "main".into(), + args: Vec::new(), + ret: Ty::unit(sp), + body: vec![ + Stmt { + span: sp, + kind: StmtKind::Let { + name: "x".into(), + ty: word, + }, + }, + Stmt { + span: sp, + kind: StmtKind::Expr(Expr::var(sp, "x", Ty::unit(sp))), + }, + ], + }], + objects: Vec::new(), + }; + + let diagnostics = check_program_with_db(&db, &program); + assert!( + diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + CheckDiagnosticKind::ExprAnnotationMismatch { .. } + )), + "{diagnostics:?}" + ); +} + +fn spanned_ident<'db>( + db: &'db TestDb, + span: Span<'db>, + name: &str, +) -> SpannedElem<'db, Ident<'db>> { + SpannedElem::new(Ident::new(db, name.to_owned()), span) +} + +fn yul_num<'db>(span: Span<'db>, value: &str) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Lit(YulLitKind::Number(value.to_owned())), + } +} + +fn yul_call<'db>( + db: &'db TestDb, + span: Span<'db>, + name: &str, + args: Vec>, +) -> YulExpr<'db> { + YulExpr { + span, + kind: YulExprKind::Call { + name: spanned_ident(db, span, name), + args, + }, + } +} + +fn yul_assign<'db>( + db: &'db TestDb, + span: Span<'db>, + names: &[&str], + value: YulExpr<'db>, +) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Assign { + names: names + .iter() + .map(|name| spanned_ident(db, span, name)) + .collect(), + value, + }, + } +} + +fn yul_expr_stmt<'db>(_db: &'db TestDb, span: Span<'db>, expr: YulExpr<'db>) -> YulStmt<'db> { + YulStmt { + span, + kind: YulStmtKind::Expr(expr), + } +} diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml new file mode 100644 index 00000000..c25b21fb --- /dev/null +++ b/crates/lsp/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "solcore-lsp" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# Core handlers over solcore-vfs are wasm-clean by default (no tokio/stdio). +default = [] +# Native stdio LSP server (tower-lsp + tokio) — never in the wasm build. +native = ["dep:tower-lsp", "dep:tokio"] +# WASM Web Worker entry (JSON-RPC over postMessage; no tokio). +wasm = ["dep:wasm-bindgen"] + +[dependencies] +vfs = { workspace = true } +hir = { workspace = true } +hir-ty = { workspace = true } +nameres = { workspace = true } +parser = { workspace = true } +# tower-lsp 0.20.0 depends on lsp-types 0.94.1, so pin the +# same exact protocol crate here to avoid duplicate LSP type versions later. +lsp-types = "=0.94.1" +line-index = "=0.1.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +url = { workspace = true } +percent-encoding = { workspace = true } +tower-lsp = { version = "0.20", optional = true } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "io-util", "sync"], optional = true } +wasm-bindgen = { version = "0.2", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +stacker = "0.1" + +[[bin]] +name = "solcore-lsp" +path = "src/bin/server.rs" +required-features = ["native"] + +# wasm-pack's bundled wasm-opt is too old for the bulk-memory ops rustc now emits. +# Size is handled by the workspace `[profile.wasm-release]` (strip + opt-level=z + lto); +# the playground `build:lsp-wasm` script runs a modern `wasm-opt` pass separately. +[package.metadata.wasm-pack.profile.custom] +wasm-opt = false diff --git a/crates/lsp/src/analysis.rs b/crates/lsp/src/analysis.rs new file mode 100644 index 00000000..10b72e8d --- /dev/null +++ b/crates/lsp/src/analysis.rs @@ -0,0 +1,23 @@ +//! Native stack guard for compiler-backed LSP requests. + +// Windows executables reserve a 1 MiB stack by default. Compiler queries can +// legitimately need more than that even for small multi-module workspaces, so +// enter a larger stack segment before starting native analysis. The red zone +// also covers Tokio's relatively small worker stacks without making the common +// Unix 8 MiB stack allocate another segment. +#[cfg(not(target_arch = "wasm32"))] +const ANALYSIS_STACK_RED_ZONE: usize = 2 * 1024 * 1024; +#[cfg(not(target_arch = "wasm32"))] +const ANALYSIS_STACK_SIZE: usize = 8 * 1024 * 1024; + +pub(crate) fn with_analysis_stack(analysis: impl FnOnce() -> T) -> T { + #[cfg(not(target_arch = "wasm32"))] + { + stacker::maybe_grow(ANALYSIS_STACK_RED_ZONE, ANALYSIS_STACK_SIZE, analysis) + } + + #[cfg(target_arch = "wasm32")] + { + analysis() + } +} diff --git a/crates/lsp/src/bin/server.rs b/crates/lsp/src/bin/server.rs new file mode 100644 index 00000000..4b584e33 --- /dev/null +++ b/crates/lsp/src/bin/server.rs @@ -0,0 +1,12 @@ +//! Native stdio entry point for the Solcore language server. + +#[cfg(feature = "native")] +#[tokio::main] +async fn main() { + solcore_lsp::native::run_stdio().await; +} + +#[cfg(not(feature = "native"))] +fn main() { + eprintln!("solcore-lsp server requires the `native` feature"); +} diff --git a/crates/lsp/src/capabilities.rs b/crates/lsp/src/capabilities.rs new file mode 100644 index 00000000..1fabcd9c --- /dev/null +++ b/crates/lsp/src/capabilities.rs @@ -0,0 +1,175 @@ +//! Static LSP capability advertisement. + +use lsp_types::{ + CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CompletionOptions, + FoldingRangeProviderCapability, HoverProviderCapability, InitializeResult, OneOf, + RenameOptions, SelectionRangeProviderCapability, SemanticTokensFullOptions, + SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities, + ServerCapabilities, ServerInfo, SignatureHelpOptions, TextDocumentSyncCapability, + TextDocumentSyncKind, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, +}; + +/// Returns the server capabilities for the transport layer's initialize reply. +pub fn server_capabilities() -> ServerCapabilities { + ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), + completion_provider: Some(CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_owned()]), + ..CompletionOptions::default() + }), + hover_provider: Some(HoverProviderCapability::Simple(true)), + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_owned(), ",".to_owned()]), + retrigger_characters: None, + work_done_progress_options: Default::default(), + }), + definition_provider: Some(OneOf::Left(true)), + references_provider: Some(OneOf::Left(true)), + document_highlight_provider: Some(OneOf::Left(true)), + rename_provider: Some(OneOf::Right(RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })), + document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { + code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), + resolve_provider: Some(false), + ..CodeActionOptions::default() + })), + document_formatting_provider: Some(OneOf::Left(true)), + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), + semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensOptions( + SemanticTokensOptions { + work_done_progress_options: Default::default(), + legend: SemanticTokensLegend { + token_types: crate::semantic_tokens::TOKEN_TYPES.to_vec(), + token_modifiers: crate::semantic_tokens::TOKEN_MODIFIERS.to_vec(), + }, + range: Some(false), + full: Some(SemanticTokensFullOptions::Bool(true)), + }, + )), + inlay_hint_provider: Some(OneOf::Left(true)), + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + ..ServerCapabilities::default() + } +} + +/// Builds an LSP initialize result with Solcore's static capabilities. +pub fn initialize_result() -> InitializeResult { + InitializeResult { + capabilities: server_capabilities(), + server_info: Some(ServerInfo { + name: "solcore-lsp".to_owned(), + version: Some(env!("CARGO_PKG_VERSION").to_owned()), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn advertises_full_sync_and_core_features() { + let capabilities = server_capabilities(); + + assert_eq!( + capabilities.text_document_sync, + Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)) + ); + let completion = capabilities + .completion_provider + .as_ref() + .expect("completion provider"); + assert_eq!(completion.resolve_provider, Some(false)); + assert_eq!(completion.trigger_characters, Some(vec![".".to_owned()])); + assert_eq!( + capabilities.hover_provider, + Some(HoverProviderCapability::Simple(true)) + ); + assert_eq!( + capabilities.signature_help_provider, + Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_owned(), ",".to_owned()]), + retrigger_characters: None, + work_done_progress_options: Default::default(), + }) + ); + assert_eq!(capabilities.definition_provider, Some(OneOf::Left(true))); + assert_eq!(capabilities.references_provider, Some(OneOf::Left(true))); + assert_eq!( + capabilities.document_highlight_provider, + Some(OneOf::Left(true)) + ); + assert_eq!( + capabilities.rename_provider, + Some(OneOf::Right(RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), + })) + ); + assert_eq!( + capabilities.document_symbol_provider, + Some(OneOf::Left(true)) + ); + assert_eq!( + capabilities.workspace_symbol_provider, + Some(OneOf::Left(true)) + ); + assert_eq!( + capabilities.code_action_provider, + Some(CodeActionProviderCapability::Options(CodeActionOptions { + code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), + resolve_provider: Some(false), + ..CodeActionOptions::default() + })) + ); + assert_eq!( + capabilities.document_formatting_provider, + Some(OneOf::Left(true)) + ); + assert_eq!( + capabilities.folding_range_provider, + Some(FoldingRangeProviderCapability::Simple(true)) + ); + assert_eq!( + capabilities.selection_range_provider, + Some(SelectionRangeProviderCapability::Simple(true)) + ); + assert_eq!( + capabilities.semantic_tokens_provider, + Some(SemanticTokensServerCapabilities::SemanticTokensOptions( + SemanticTokensOptions { + work_done_progress_options: Default::default(), + legend: SemanticTokensLegend { + token_types: crate::semantic_tokens::TOKEN_TYPES.to_vec(), + token_modifiers: crate::semantic_tokens::TOKEN_MODIFIERS.to_vec(), + }, + range: Some(false), + full: Some(SemanticTokensFullOptions::Bool(true)), + } + )) + ); + assert_eq!(capabilities.inlay_hint_provider, Some(OneOf::Left(true))); + assert_eq!( + capabilities.workspace, + Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }) + ); + } +} diff --git a/crates/lsp/src/code_actions.rs b/crates/lsp/src/code_actions.rs new file mode 100644 index 00000000..35a5b5c3 --- /dev/null +++ b/crates/lsp/src/code_actions.rs @@ -0,0 +1,1685 @@ +//! Diagnostic quick fixes over the wasm-clean LSP core. + +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, +}; + +use hir::{ + diag::{AnyDiagnostic, LabelSpan}, + nameres::{NameresDiagnostic, UndefinedNameKind}, +}; +use lsp_types::{ + CodeAction, CodeActionContext, CodeActionKind, CodeActionOrCommand, CodeActionResponse, + Diagnostic as LspDiagnostic, Position, Range, TextEdit, Url, WorkspaceEdit, +}; +use nameres::Db as _; +use vfs::{DiagnosticSuggestion, DiagnosticTextEdit, SuggestionApplicability}; + +use crate::{ + analysis::with_analysis_stack, + diagnostics::{compute_vfs_diagnostics, to_lsp_diagnostic}, + import_edits::{plan_import_edit, plan_module_import_edit}, + resolve::module_id_for_uri, + state::WorldState, +}; + +const MAX_AUTO_IMPORT_CANDIDATES: usize = 20; + +/// Computes compiler-provided quick fixes for diagnostics in an LSP request. +/// +/// The request context must contain the same diagnostic code, range, and +/// message that Solcore currently publishes. This prevents a stale diagnostic +/// from applying an edit after the document has changed. +pub fn handle_code_action( + world: &WorldState, + uri: &Url, + range: Range, + context: &CodeActionContext, +) -> Option { + with_analysis_stack(|| handle_code_action_inner(world, uri, range, context)) +} + +fn handle_code_action_inner( + world: &WorldState, + uri: &Url, + range: Range, + context: &CodeActionContext, +) -> Option { + let line_index = world.line_index(uri)?; + let request_start = line_index.position_to_byte(range.start)?; + let request_end = line_index.position_to_byte(range.end)?; + if request_start > request_end { + return None; + } + if !quick_fixes_requested(context) || context.diagnostics.is_empty() { + return Some(Vec::new()); + } + + let db = world.db(); + let current_module = module_id_for_uri(world, db, uri); + let mut actions = Vec::new(); + let mut seen = HashSet::new(); + for diagnostic in compute_vfs_diagnostics(world, uri) { + let published = to_lsp_diagnostic(world, line_index, diagnostic.clone()); + if !ranges_intersect(range, published.range) { + continue; + } + let Some(request_diagnostic) = context + .diagnostics + .iter() + .find(|candidate| diagnostic_matches(candidate, &published)) + else { + continue; + }; + + let mut suggestions = diagnostic.suggestions.clone(); + if let Some(module) = current_module { + suggestions.extend(auto_import_suggestions(db, module, &diagnostic)); + } + + for suggestion in &suggestions { + if matches!( + suggestion.applicability, + SuggestionApplicability::HasPlaceholders | SuggestionApplicability::Unspecified + ) { + continue; + } + let Some((edit, edit_key)) = suggestion_workspace_edit(world, suggestion) else { + continue; + }; + if !seen.insert((suggestion.title.clone(), edit_key)) { + continue; + } + actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: suggestion.title.clone(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![request_diagnostic.clone()]), + edit: Some(edit), + command: None, + is_preferred: Some(matches!( + suggestion.applicability, + SuggestionApplicability::MachineApplicable + )), + disabled: None, + data: None, + })); + } + } + + Some(actions) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum MissingImport { + Name { + name: String, + namespace: nameres::Namespace, + }, + QualifiedConstructor { + type_name: String, + constructor_name: String, + }, + QualifiedAccess { + qualifier: String, + member: String, + }, + ModuleMember { + qualifier: String, + member: String, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct PlannedImport { + title: String, + edit: crate::import_edits::ImportEdit, +} + +fn auto_import_suggestions<'db>( + db: &'db vfs::AnalysisHost, + current_module: nameres::ModuleId<'db>, + diagnostic: &vfs::Diagnostic, +) -> Vec { + let Some(missing) = missing_import_for_diagnostic(db, current_module, diagnostic) else { + return Vec::new(); + }; + + let Some(file) = db.module_file(current_module) else { + return Vec::new(); + }; + let Some(source) = file.content(db).as_deref() else { + return Vec::new(); + }; + let parsed = parser::parse_file_to_hir(db, file); + let mut planned = Vec::new(); + match missing { + MissingImport::Name { name, namespace } => { + if !parser::is_valid_identifier(&name) { + return Vec::new(); + } + extend_symbol_imports( + db, + source, + parsed, + nameres::auto_import_candidates(db, current_module, &name, namespace), + &mut planned, + ); + } + MissingImport::QualifiedConstructor { + type_name, + constructor_name, + } => { + extend_symbol_imports( + db, + source, + parsed, + nameres::auto_import_constructor_candidates( + db, + current_module, + &type_name, + &constructor_name, + ), + &mut planned, + ); + } + MissingImport::QualifiedAccess { qualifier, member } => { + extend_symbol_imports( + db, + source, + parsed, + nameres::auto_import_candidates( + db, + current_module, + &qualifier, + nameres::Namespace::Class, + ), + &mut planned, + ); + extend_symbol_imports( + db, + source, + parsed, + nameres::auto_import_constructor_candidates( + db, + current_module, + &qualifier, + &member, + ), + &mut planned, + ); + extend_module_imports( + db, + current_module, + source, + parsed, + &qualifier, + &member, + &mut planned, + ); + } + MissingImport::ModuleMember { qualifier, member } => { + extend_module_imports( + db, + current_module, + source, + parsed, + &qualifier, + &member, + &mut planned, + ); + } + } + + let machine_applicable = planned.len() == 1 && diagnostic.suggestions.is_empty(); + let file_url = file.url(db).as_str().to_owned(); + planned + .into_iter() + .map(|planned| DiagnosticSuggestion { + title: planned.title, + applicability: if machine_applicable { + SuggestionApplicability::MachineApplicable + } else { + SuggestionApplicability::MaybeIncorrect + }, + edits: vec![DiagnosticTextEdit { + range: vfs::DiagRange { + file_url: file_url.clone(), + start: planned.edit.start, + end: planned.edit.end, + }, + replacement: planned.edit.replacement, + }], + }) + .collect() +} + +fn extend_symbol_imports<'db>( + db: &'db vfs::AnalysisHost, + source: &str, + parsed: parser::ParseHirOutput<'db>, + candidates: Vec>, + planned: &mut Vec, +) { + for candidate in candidates { + if planned.len() == MAX_AUTO_IMPORT_CANDIDATES { + return; + } + let Some(edit) = plan_import_edit( + db, + source, + parsed, + &candidate.import_path, + &candidate.public_name, + ) else { + continue; + }; + planned.push(PlannedImport { + title: format!( + "Import `{}` from `{}`", + candidate.public_name, candidate.import_path + ), + edit, + }); + } +} + +fn extend_module_imports<'db>( + db: &'db vfs::AnalysisHost, + current_module: nameres::ModuleId<'db>, + source: &str, + parsed: parser::ParseHirOutput<'db>, + qualifier: &str, + member: &str, + planned: &mut Vec, +) { + for candidate in nameres::auto_import_module_candidates(db, current_module, qualifier, member) { + if planned.len() == MAX_AUTO_IMPORT_CANDIDATES { + return; + } + let Some(edit) = plan_module_import_edit(db, source, parsed, &candidate.import_path) else { + continue; + }; + planned.push(PlannedImport { + title: format!( + "Import module `{}` from `{}`", + candidate.qualifier, candidate.import_path + ), + edit, + }); + } +} + +fn missing_import_for_diagnostic<'db>( + db: &'db vfs::AnalysisHost, + module: nameres::ModuleId<'db>, + diagnostic: &vfs::Diagnostic, +) -> Option { + let primary = diagnostic.primary.as_ref()?; + // Source declarations are resolved by `nameres`, while compiler-generated + // contract entries are resolved during type checking. Both retain the same + // structured name-resolution diagnostic, so auto-imports can treat them + // uniformly without knowing which module provides the missing symbol. + nameres::module_diagnostics(db, module) + .iter() + .chain(hir_ty::infer::module_typeck_diagnostics(db, module).iter()) + .find_map(|any_diagnostic| { + let rendered_message = any_diagnostic.lower(db).message; + let AnyDiagnostic::Nameres(candidate) = any_diagnostic else { + return None; + }; + let (missing, span, code) = match candidate { + NameresDiagnostic::UndefinedName { + name, + span, + kind: UndefinedNameKind::Term, + .. + } => ( + MissingImport::Name { + name: name.clone(), + namespace: nameres::Namespace::Term, + }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME, + ), + NameresDiagnostic::UndefinedName { + span, + kind: UndefinedNameKind::ModuleQualifier { access_path }, + .. + } => { + let (qualifier, member) = qualified_path_segments(access_path)?; + ( + MissingImport::QualifiedAccess { qualifier, member }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME, + ) + } + NameresDiagnostic::UndefinedName { + span, + kind: UndefinedNameKind::ModuleMember { access_path }, + .. + } => { + let (qualifier, member) = qualified_path_segments(access_path)?; + ( + MissingImport::ModuleMember { qualifier, member }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME, + ) + } + NameresDiagnostic::UndefinedName { + span, + kind: UndefinedNameKind::QualifiedConstructor { access_path }, + .. + } => { + let (type_name, constructor_name) = qualified_path_segments(access_path)?; + ( + MissingImport::QualifiedConstructor { + type_name, + constructor_name, + }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME, + ) + } + NameresDiagnostic::UndefinedTypeConstructor { name, span, .. } => ( + MissingImport::Name { + name: name.clone(), + namespace: nameres::Namespace::Type, + }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR, + ), + NameresDiagnostic::UndefinedClass { name, span } => ( + MissingImport::Name { + name: name.clone(), + namespace: nameres::Namespace::Class, + }, + span, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_CLASS, + ), + _ => return None, + }; + (diagnostic.code.as_deref() == Some(code) + && diagnostic.message == rendered_message + && diagnostic_span_matches(db, span, primary)) + .then_some(missing) + }) +} + +fn qualified_path_segments(access_path: &str) -> Option<(String, String)> { + let mut segments = access_path.split('.'); + let qualifier = segments.next()?; + let member = segments.next()?; + if segments.next().is_some() + || !parser::is_valid_identifier(qualifier) + || !parser::is_valid_identifier(member) + { + return None; + } + + Some((qualifier.to_owned(), member.to_owned())) +} + +fn diagnostic_span_matches( + db: &vfs::AnalysisHost, + span: &LabelSpan, + range: &vfs::DiagRange, +) -> bool { + let absolute = span.resolve_to_absolute(db); + absolute.file().url(db).as_str() == range.file_url + && absolute.start().as_u32() == range.start + && absolute.end().as_u32() == range.end +} + +fn quick_fixes_requested(context: &CodeActionContext) -> bool { + context.only.as_ref().is_none_or(|only| { + only.iter().any(|requested| { + requested == &CodeActionKind::EMPTY + || requested == &CodeActionKind::QUICKFIX + || CodeActionKind::QUICKFIX + .as_str() + .strip_prefix(requested.as_str()) + .is_some_and(|suffix| suffix.starts_with('.')) + }) + }) +} + +fn diagnostic_matches(candidate: &LspDiagnostic, published: &LspDiagnostic) -> bool { + candidate.range == published.range + && candidate.code == published.code + && candidate.message == published.message + && candidate + .source + .as_deref() + .is_none_or(|source| source == "solcore") +} + +fn suggestion_workspace_edit( + world: &WorldState, + suggestion: &DiagnosticSuggestion, +) -> Option<(WorkspaceEdit, Vec)> { + if suggestion.edits.is_empty() { + return None; + } + + let mut changes = HashMap::>::new(); + let mut key = Vec::with_capacity(suggestion.edits.len()); + for edit in &suggestion.edits { + let (uri, text_edit, changes_text) = to_lsp_text_edit(world, edit)?; + if !changes_text { + continue; + } + key.push(EditKey::new(&uri, &text_edit)); + changes.entry(uri).or_default().push(text_edit); + } + if key.is_empty() { + return None; + } + + key.sort(); + key.dedup(); + for edits in changes.values_mut() { + edits.sort_by(|left, right| compare_ranges(&left.range, &right.range)); + edits.dedup(); + if edits + .windows(2) + .any(|pair| text_edits_conflict(&pair[0], &pair[1])) + { + return None; + } + } + + Some(( + WorkspaceEdit { + changes: Some(changes), + document_changes: None, + change_annotations: None, + }, + key, + )) +} + +fn text_edits_conflict(left: &TextEdit, right: &TextEdit) -> bool { + positions_cmp(right.range.start, left.range.end).is_lt() + || (left.range.start == left.range.end + && right.range.start == right.range.end + && left.range.start == right.range.start) +} + +fn to_lsp_text_edit( + world: &WorldState, + edit: &DiagnosticTextEdit, +) -> Option<(Url, TextEdit, bool)> { + let uri = world.client_uri_for_vfs_url(&edit.range.file_url)?; + let line_index = world.line_index(&uri)?; + let start = usize::try_from(edit.range.start).ok()?; + let end = usize::try_from(edit.range.end).ok()?; + let text = line_index.text(); + if start > end + || end > text.len() + || !text.is_char_boundary(start) + || !text.is_char_boundary(end) + { + return None; + } + + Some(( + uri, + TextEdit { + range: line_index.range(edit.range.start, edit.range.end), + new_text: edit.replacement.clone(), + }, + text[start..end] != edit.replacement, + )) +} + +fn ranges_intersect(left: Range, right: Range) -> bool { + if left.start == left.end { + return !positions_cmp(left.start, right.start).is_lt() + && !positions_cmp(left.start, right.end).is_gt(); + } + if right.start == right.end { + return !positions_cmp(right.start, left.start).is_lt() + && !positions_cmp(right.start, left.end).is_gt(); + } + + positions_cmp(left.start, right.end).is_lt() && positions_cmp(right.start, left.end).is_lt() +} + +fn compare_ranges(left: &Range, right: &Range) -> Ordering { + positions_cmp(left.start, right.start).then_with(|| positions_cmp(left.end, right.end)) +} + +fn positions_cmp(left: Position, right: Position) -> Ordering { + left.line + .cmp(&right.line) + .then_with(|| left.character.cmp(&right.character)) +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct EditKey { + uri: String, + start_line: u32, + start_character: u32, + end_line: u32, + end_character: u32, + replacement: String, +} + +impl EditKey { + fn new(uri: &Url, edit: &TextEdit) -> Self { + Self { + uri: uri.as_str().to_owned(), + start_line: edit.range.start.line, + start_character: edit.range.start.character, + end_line: edit.range.end.line, + end_character: edit.range.end.character, + replacement: edit.new_text.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use lsp_types::{CodeActionContext, NumberOrString}; + + use super::*; + use crate::diagnostics::compute_diagnostics; + + const WINDOWS_TEST_STACK_SIZE: usize = 1024 * 1024; + + fn on_windows_sized_stack(test: fn()) { + let result = std::thread::Builder::new() + .stack_size(WINDOWS_TEST_STACK_SIZE) + .spawn(test) + .expect("spawn Windows-sized LSP test stack") + .join(); + if let Err(payload) = result { + std::panic::resume_unwind(payload); + } + } + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn undefined_name_diagnostic(world: &WorldState, uri: &Url) -> LspDiagnostic { + diagnostic_with_code( + world, + uri, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME, + ) + } + + fn diagnostic_with_code(world: &WorldState, uri: &Url, code: &str) -> LspDiagnostic { + compute_diagnostics(world, uri) + .into_iter() + .find(|diagnostic| diagnostic.code == Some(NumberOrString::String(code.to_owned()))) + .unwrap_or_else(|| panic!("missing diagnostic {code}")) + } + + fn context(diagnostic: LspDiagnostic) -> CodeActionContext { + CodeActionContext { + diagnostics: vec![diagnostic], + only: None, + trigger_kind: None, + } + } + + fn action(actions: &[CodeActionOrCommand]) -> &CodeAction { + match actions { + [CodeActionOrCommand::CodeAction(action)] => action, + other => panic!("expected one code action, got {other:#?}"), + } + } + + #[test] + fn typo_diagnostic_becomes_nonpreferred_quick_fix() { + let source = "function value() returns (word) { return 1; }\nfunction main() returns (word) { return vaue(); }\n"; + let (world, uri) = world_with_main(source); + let diagnostic = undefined_name_diagnostic(&world, &uri); + let requested_range = diagnostic.range; + + let actions = + handle_code_action(&world, &uri, requested_range, &context(diagnostic.clone())) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Replace with `value`"); + assert_eq!(action.kind, Some(CodeActionKind::QUICKFIX)); + assert_eq!(action.diagnostics, Some(vec![diagnostic.clone()])); + assert_eq!(action.is_preferred, Some(false)); + let edit = action.edit.as_ref().expect("workspace edit"); + let changes = edit.changes.as_ref().expect("changes"); + assert_eq!( + changes.get(&uri), + Some(&vec![TextEdit { + range: diagnostic.range, + new_text: "value".to_owned(), + }]) + ); + } + + #[test] + fn real_uri_and_utf16_range_are_preserved() { + let source = "// 😀\nfunction value() returns (word) { return 1; }\nfunction main() returns (word) { return vaue(); }\n"; + let root = Url::parse("file:///tmp/solcore%20project/").expect("root uri"); + let uri = + Url::parse("file:///tmp/solcore%20project/src/%E6%95%B0.solc").expect("document uri"); + let mut world = WorldState::new(); + assert_eq!( + world.load_workspace_documents(root, [(uri.clone(), source.to_owned())]), + 1 + ); + assert!(world.open_document(uri.clone(), source.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &uri); + + let actions = + handle_code_action(&world, &uri, diagnostic.range, &context(diagnostic.clone())) + .expect("code-action response"); + let changes = action(&actions) + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .expect("changes"); + + assert_eq!(changes.keys().collect::>(), vec![&uri]); + assert_eq!(changes[&uri][0].range, diagnostic.range); + } + + #[test] + fn stale_code_or_range_does_not_receive_a_fix() { + let source = "function value() returns (word) { return 1; }\nfunction main() returns (word) { return vaue(); }\n"; + let (world, uri) = world_with_main(source); + let diagnostic = undefined_name_diagnostic(&world, &uri); + + let mut wrong_code = diagnostic.clone(); + wrong_code.code = Some(NumberOrString::String("SC9999".to_owned())); + assert_eq!( + handle_code_action(&world, &uri, diagnostic.range, &context(wrong_code)), + Some(Vec::new()) + ); + + let mut wrong_range = diagnostic.clone(); + wrong_range.range = Range::new(Position::new(0, 0), Position::new(0, 1)); + assert_eq!( + handle_code_action(&world, &uri, diagnostic.range, &context(wrong_range)), + Some(Vec::new()) + ); + } + + #[test] + fn typed_missing_import_lookup_requires_the_same_diagnostic_code() { + let (world, uri) = world_with_main("function main() returns (word) { return missing; }\n"); + let db = world.db(); + let module = module_id_for_uri(&world, db, &uri).expect("main module"); + let mut diagnostic = compute_vfs_diagnostics(&world, &uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code.as_deref() + == Some(hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME) + }) + .expect("undefined-name diagnostic"); + + assert_eq!( + missing_import_for_diagnostic(db, module, &diagnostic), + Some(MissingImport::Name { + name: "missing".to_owned(), + namespace: nameres::Namespace::Term, + }) + ); + diagnostic.code = + Some(hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR.to_owned()); + assert_eq!(missing_import_for_diagnostic(db, module, &diagnostic), None); + } + + #[test] + fn request_range_and_only_filter_are_respected() { + let source = "function value() returns (word) { return 1; }\nfunction main() returns (word) { return vaue(); }\n"; + let (world, uri) = world_with_main(source); + let diagnostic = undefined_name_diagnostic(&world, &uri); + + assert_eq!( + handle_code_action( + &world, + &uri, + Range::new(Position::new(0, 0), Position::new(0, 1)), + &context(diagnostic.clone()), + ), + Some(Vec::new()) + ); + + let source_only = CodeActionContext { + diagnostics: vec![diagnostic.clone()], + only: Some(vec![CodeActionKind::SOURCE]), + trigger_kind: None, + }; + assert_eq!( + handle_code_action(&world, &uri, diagnostic.range, &source_only), + Some(Vec::new()) + ); + + let quickfix_only = CodeActionContext { + diagnostics: vec![diagnostic.clone()], + only: Some(vec![CodeActionKind::QUICKFIX]), + trigger_kind: None, + }; + assert_eq!( + handle_code_action(&world, &uri, diagnostic.range, &quickfix_only) + .expect("code-action response") + .len(), + 1 + ); + } + + #[test] + fn unknown_import_item_uses_compiler_suggestion() { + let main = "import {doubl} from math;\nfunction main() returns (word) { return 1; }\n"; + let math = + "function double(x: word) returns (word) { return x + x; }\nexport { double };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostic = compute_diagnostics(&world, &main_uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::MODULE_UNKNOWN_IMPORT_ITEM.to_owned(), + )) + }) + .expect("unknown-import-item diagnostic"); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Replace with `double`"); + assert_eq!(action.is_preferred, Some(false)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: diagnostic.range, + new_text: "double".to_owned(), + }]) + ); + } + + #[test] + fn module_path_typo_is_nonpreferred() { + let main = "import * as mth from mth;\nfunction main() returns (word) { return 1; }\n"; + let math = + "function double(x: word) returns (word) { return x + x; }\nexport { double };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostic = compute_diagnostics(&world, &main_uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::MODULE_NOT_FOUND.to_owned(), + )) + }) + .expect("module-not-found diagnostic"); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Replace with `math`"); + assert_eq!(action.is_preferred, Some(false)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: diagnostic.range, + new_text: "math".to_owned(), + }]) + ); + } + + #[test] + fn qualified_name_suggestion_replaces_only_the_leaf() { + let main = "import * as math from math;\nfunction main(x: math.Vaue) returns (word) { return 1; }\n"; + let math = "enum Value { Value(word) }\nexport { Value(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostic = compute_diagnostics(&world, &main_uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR.to_owned(), + )) + }) + .expect("undefined-type-constructor diagnostic"); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Replace with `Value`"); + assert_eq!(action.is_preferred, Some(false)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: diagnostic.range, + new_text: "Value".to_owned(), + }]) + ); + } + + #[test] + fn qualified_name_with_wrong_qualifier_has_no_partial_fix() { + let main = + "import * as M from math;\nfunction main(x: N.Value) returns (word) { return 1; }\n"; + let math = "enum Value { Value(word) }\nexport { Value(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostic = compute_diagnostics(&world, &main_uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR.to_owned(), + )) + }) + .expect("undefined-type-constructor diagnostic"); + + assert!(diagnostic.message.contains("did you mean type `M.Value`?")); + assert_eq!( + handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ), + Some(Vec::new()) + ); + } + + #[test] + fn qualified_name_with_wrong_qualifier_and_leaf_has_no_partial_fix() { + let main = + "import * as M from math;\nfunction main(x: N.Vaue) returns (word) { return 1; }\n"; + let math = "enum Value { Value(word) }\nexport { Value(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostic = compute_diagnostics(&world, &main_uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR.to_owned(), + )) + }) + .expect("undefined-type-constructor diagnostic"); + + assert!(diagnostic.message.contains("did you mean type `M.Value`?")); + assert_eq!( + handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ), + Some(Vec::new()) + ); + } + + #[test] + fn exact_constructor_qualification_is_preferred() { + let source = "enum Option { None, Some(word) }\nfunction main(x: word) returns (Option) { return Some(x); }\n// migrate-syntax: keep-unqualified-constructor\n"; + let (world, uri) = world_with_main(source); + let diagnostic = compute_diagnostics(&world, &uri) + .into_iter() + .find(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNQUALIFIED_CONSTRUCTOR.to_owned(), + )) + }) + .expect("unqualified-constructor diagnostic"); + + let actions = + handle_code_action(&world, &uri, diagnostic.range, &context(diagnostic.clone())) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Replace with `Option.Some`"); + assert_eq!(action.is_preferred, Some(true)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&uri)), + Some(&vec![TextEdit { + range: diagnostic.range, + new_text: "Option.Some".to_owned(), + }]) + ); + } + + #[test] + fn no_op_suggestion_edits_are_not_emitted() { + let source = "function main() returns (word) { return 1; }\n"; + let (world, uri) = world_with_main(source); + let suggestion = DiagnosticSuggestion { + title: "No change".to_owned(), + applicability: SuggestionApplicability::MachineApplicable, + edits: vec![DiagnosticTextEdit { + range: vfs::DiagRange { + file_url: uri.as_str().to_owned(), + start: 0, + end: "function".len() as u32, + }, + replacement: "function".to_owned(), + }], + }; + + assert!(suggestion_workspace_edit(&world, &suggestion).is_none()); + } + + #[test] + fn unique_exported_term_gets_a_preferred_auto_import() { + let main = "function main() returns (word) { return value(); }\n"; + let math = "function value() returns (word) { return 1; }\nexport { value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("code-action response"); + let action = action(&actions); + + assert_eq!(action.title, "Import `value` from `lib.math`"); + assert_eq!(action.is_preferred, Some(true)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(0, 0)), + new_text: "import {value} from lib.math;\n".to_owned(), + }]) + ); + + let fixed = format!("import {{value}} from lib.math;\n{main}"); + let mut fixed_world = WorldState::new(); + assert!(fixed_world.open_document(main_uri.clone(), fixed)); + assert!(fixed_world.open_document(math_uri, math.to_owned())); + assert!(compute_diagnostics(&fixed_world, &main_uri).iter().all( + |diagnostic| diagnostic.code + != Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME.to_owned() + )) + )); + } + + #[test] + fn multiple_auto_import_providers_are_sorted_and_nonpreferred() { + let main = "function main() returns (word) { return value(); }\n"; + let provider = "function value() returns (word) { return 1; }\nexport { value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math.solc").expect("math uri"), + provider.to_owned() + )); + assert!(world.open_document( + Url::parse("file:///main/util.solc").expect("util uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("code-action response"); + let actions = actions + .iter() + .map(|action| match action { + CodeActionOrCommand::CodeAction(action) => action, + CodeActionOrCommand::Command(_) => panic!("expected code action"), + }) + .collect::>(); + + assert_eq!( + actions + .iter() + .map(|action| action.title.as_str()) + .collect::>(), + [ + "Import `value` from `lib.math`", + "Import `value` from `lib.util`" + ] + ); + assert!( + actions + .iter() + .all(|action| action.is_preferred == Some(false)) + ); + } + + #[test] + fn auto_import_extends_an_existing_selective_import() { + on_windows_sized_stack(auto_import_extends_an_existing_selective_import_inner); + } + + fn auto_import_extends_an_existing_selective_import_inner() { + let main = + "import {other} from lib.math;\nfunction main() returns (word) { return value(); }\n"; + let math = "function other() returns (word) { return 0; }\nfunction value() returns (word) { return 1; }\nexport { other, value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math.solc").expect("math uri"), + math.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + let insertion = (main.find("other").expect("selector") + "other".len()) as u32; + let expected_range = world + .line_index(&main_uri) + .expect("line index") + .range(insertion, insertion); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("code-action response"); + let edits = action(&actions) + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)) + .expect("main edits"); + + assert_eq!( + edits, + &vec![TextEdit { + range: expected_range, + new_text: ", value".to_owned(), + }] + ); + } + + #[test] + fn exported_types_and_classes_are_auto_importable() { + let type_main = "function keep(x: Token) returns (Token) { return x; }\n"; + let type_provider = "enum Token { Token(word) }\nexport { Token };\n"; + let mut type_world = WorldState::new(); + let type_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(type_world.open_document(type_uri.clone(), type_main.to_owned())); + assert!(type_world.open_document( + Url::parse("file:///main/model.solc").expect("model uri"), + type_provider.to_owned() + )); + let type_diagnostic = diagnostic_with_code( + &type_world, + &type_uri, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR, + ); + let type_actions = handle_code_action( + &type_world, + &type_uri, + type_diagnostic.range, + &context(type_diagnostic), + ) + .expect("type code actions"); + assert_eq!( + action(&type_actions).title, + "Import `Token` from `lib.model`" + ); + + let class_main = "function keep(x: a) returns (a) where a: Comparable { return x; }\n"; + let class_provider = "trait Comparable {\n function compare(x: a, y: a) returns (word);\n}\nexport { Comparable };\n"; + let mut class_world = WorldState::new(); + let class_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(class_world.open_document(class_uri.clone(), class_main.to_owned())); + assert!(class_world.open_document( + Url::parse("file:///main/classes.solc").expect("classes uri"), + class_provider.to_owned() + )); + let class_diagnostic = diagnostic_with_code( + &class_world, + &class_uri, + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_CLASS, + ); + let class_actions = handle_code_action( + &class_world, + &class_uri, + class_diagnostic.range, + &context(class_diagnostic), + ) + .expect("trait code actions"); + assert_eq!( + action(&class_actions).title, + "Import `Comparable` from `lib.classes`" + ); + } + + #[test] + fn generated_dispatch_missing_type_and_class_have_auto_import_candidates() { + let source = r#"import std; +import {address as address_} from std.opcodes; + +contract C { + constructor() {} + function nothing() public {} +} +"#; + let (world, uri) = world_with_main(source); + + for (code, expected_title) in [ + ( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_TYPE_CONSTRUCTOR, + "Import `NonPayable` from `std.dispatch`", + ), + ( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_CLASS, + "Import `SigString` from `std.dispatch`", + ), + ] { + let diagnostic = diagnostic_with_code(&world, &uri, code); + let actions = handle_code_action(&world, &uri, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + let action = action(&actions); + assert_eq!(action.title, expected_title); + assert_eq!(action.is_preferred, Some(true)); + } + } + + #[test] + fn generated_dispatch_missing_terms_have_auto_import_candidates() { + let source = r#"import std; +import {address as address_} from std.opcodes; +import {NonPayable, SigString} from std.dispatch; + +contract C { + constructor() {} + function nothing() public {} +} +"#; + let (world, uri) = world_with_main(source); + let diagnostics = compute_diagnostics(&world, &uri) + .into_iter() + .filter(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME.to_owned(), + )) + }) + .collect::>(); + assert!( + !diagnostics.is_empty(), + "expected generated term diagnostics" + ); + + for message in [ + "undefined name: Contract", + "undefined name: Fallback", + "undefined name: Method", + ] { + let diagnostic = diagnostics + .iter() + .find(|diagnostic| diagnostic.message.starts_with(message)) + .unwrap_or_else(|| panic!("missing diagnostic `{message}`")) + .clone(); + let actions = + handle_code_action(&world, &uri, diagnostic.range, &context(diagnostic.clone())) + .expect("code actions"); + assert!( + actions.iter().all(|action| !matches!( + action, + CodeActionOrCommand::CodeAction(action) + if action.title.starts_with("Import all from ") + )), + "unqualified constructor `{message}` must not receive a wildcard import: {actions:#?}" + ); + } + + for (message, expected_title) in [ + ( + "undefined name: RunContract", + "Import `RunContract` from `std.dispatch`", + ), + ( + "undefined name: fallback_default_implementation", + "Import `fallback_default_implementation` from `std.dispatch`", + ), + ] { + let diagnostic = diagnostics + .iter() + .find(|diagnostic| diagnostic.message.starts_with(message)) + .unwrap_or_else(|| { + panic!( + "missing diagnostic `{message}` in {:?}", + diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect::>() + ) + }) + .clone(); + let actions = + handle_code_action(&world, &uri, diagnostic.range, &context(diagnostic.clone())) + .expect("code actions"); + assert!( + actions.iter().any(|action| matches!( + action, + CodeActionOrCommand::CodeAction(action) if action.title == expected_title + )), + "missing `{expected_title}` for {message}: {actions:#?}" + ); + } + } + + #[test] + fn resolved_member_errors_do_not_offer_term_imports() { + let field_main = + "enum Local { Present }\nfunction main() returns (word) { return Local.missing; }\n"; + let exported_missing = + "function missing() returns (word) { return 1; }\nexport { missing };\n"; + let mut field_world = WorldState::new(); + let field_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(field_world.open_document(field_uri.clone(), field_main.to_owned())); + assert!(field_world.open_document( + Url::parse("file:///main/symbols.solc").expect("symbols uri"), + exported_missing.to_owned() + )); + let field_diagnostic = undefined_name_diagnostic(&field_world, &field_uri); + assert_eq!( + handle_code_action( + &field_world, + &field_uri, + field_diagnostic.range, + &context(field_diagnostic), + ), + Some(Vec::new()) + ); + } + + #[test] + fn resolved_module_member_does_not_offer_a_constructor_import() { + let main = "import * as Math from lib.foo;\nfunction main() returns (word) { return Math.Value(1); }\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/foo.solc").expect("foo uri"), + "function other() returns (word) { return 0; }\nexport { other };\n".to_owned() + )); + assert!(world.open_document( + Url::parse("file:///main/model.solc").expect("model uri"), + "enum Math { Value(word) }\nexport { Math(*) };\n".to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + assert!(actions.iter().all(|action| match action { + CodeActionOrCommand::CodeAction(action) => !action.title.starts_with("Import "), + CodeActionOrCommand::Command(_) => true, + })); + } + + #[test] + fn qualified_constructor_expression_imports_the_visible_type() { + let main = "function main() returns (word) { let option = Option.Some(1); return 1; }\n"; + let provider = "enum Option { None, Some(word) }\nexport { Option(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let model_uri = Url::parse("file:///main/model.solc").expect("model uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(model_uri.clone(), provider.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("constructor code actions"); + let action = action(&actions); + + assert_eq!(action.title, "Import `Option` from `lib.model`"); + assert_eq!(action.is_preferred, Some(true)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(0, 0)), + new_text: "import {Option} from lib.model;\n".to_owned(), + }]) + ); + + let mut fixed_world = WorldState::new(); + assert!(fixed_world.open_document( + main_uri.clone(), + format!("import {{Option}} from lib.model;\n{main}"), + )); + assert!(fixed_world.open_document(model_uri, provider.to_owned())); + assert!(compute_diagnostics(&fixed_world, &main_uri).iter().all( + |diagnostic| diagnostic.code + != Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME.to_owned() + )) + )); + } + + #[test] + fn qualified_constructor_pattern_imports_the_visible_type() { + let main = "function main(x: word) returns (word) {\n match (x) { case Option.Some(value) { return value; } default { return 0; } }\n}\n"; + let provider = "enum Option { None, Some(word) }\nexport { Option(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let model_uri = Url::parse("file:///main/model.solc").expect("model uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(model_uri, provider.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("pattern constructor code actions"); + + assert_eq!(action(&actions).title, "Import `Option` from `lib.model`"); + } + + #[test] + fn resolved_pattern_type_does_not_import_a_conflicting_constructor_owner() { + let main = "enum Option { None }\nfunction main(x: word) returns (word) {\n match (x) { case Option.Some(value) { return value; } default { return 0; } }\n}\n"; + let provider = "enum Option { None, Some(word) }\nexport { Option(*) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/model.solc").expect("model uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + assert!(actions.iter().all(|action| match action { + CodeActionOrCommand::CodeAction(action) => !action.title.starts_with("Import "), + CodeActionOrCommand::Command(_) => true, + })); + } + + #[test] + fn qualified_constructor_import_requires_that_constructor_to_be_exported() { + let main = "function main() returns (word) { let option = Option.Some(1); return 1; }\n"; + let provider = "enum Option { None, Some(word) }\nexport { Option(None) };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/model.solc").expect("model uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + assert_eq!( + handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic),), + Some(Vec::new()) + ); + } + + #[test] + fn module_import_requires_an_immediate_term_member() { + let main = "function main() returns (word) { return math.Value; }\n"; + let provider = "enum Value { Value(word) }\nexport { Value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math.solc").expect("math uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + assert!(actions.iter().all(|action| match action { + CodeActionOrCommand::CodeAction(action) => !action.title.starts_with("Import "), + CodeActionOrCommand::Command(_) => true, + })); + } + + #[test] + fn missing_module_qualifier_gets_a_namespace_import() { + let main = "function main() returns (word) { return math.value(); }\n"; + let provider = "function value() returns (word) { return 1; }\nexport { value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), provider.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + let actions = handle_code_action( + &world, + &main_uri, + diagnostic.range, + &context(diagnostic.clone()), + ) + .expect("module code actions"); + let action = action(&actions); + + assert_eq!(action.title, "Import module `math` from `lib.math`"); + assert_eq!(action.is_preferred, Some(true)); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(0, 0)), + new_text: "import * as math from lib.math;\n".to_owned(), + }]) + ); + + let mut fixed_world = WorldState::new(); + assert!(fixed_world.open_document( + main_uri.clone(), + format!("import * as math from lib.math;\n{main}"), + )); + assert!(fixed_world.open_document(math_uri, provider.to_owned())); + assert!(compute_diagnostics(&fixed_world, &main_uri).iter().all( + |diagnostic| diagnostic.code + != Some(NumberOrString::String( + hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME.to_owned() + )) + )); + } + + #[test] + fn namespace_import_stays_separate_from_an_existing_selective_import() { + let main = "import {other} from lib.math;\nfunction main() returns (word) { return math.value(); }\n"; + let provider = "function other() returns (word) { return 0; }\nfunction value() returns (word) { return 1; }\nexport { other, value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math.solc").expect("math uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + let insertion = main.find('\n').expect("import line end") as u32 + 1; + let expected_range = world + .line_index(&main_uri) + .expect("line index") + .range(insertion, insertion); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("module code actions"); + + assert_eq!( + action(&actions) + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: expected_range, + new_text: "import * as math from lib.math;\n".to_owned(), + }]) + ); + } + + #[test] + fn namespace_import_does_not_conflict_with_an_unqualified_term() { + let main = "import {other} from lib.math;\nalias math = word;\nfunction main() returns (word) { return math.value(); }\n"; + let provider = "function other() returns (word) { return 0; }\nfunction value() returns (word) { return 1; }\nexport { other, value };\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math.solc").expect("math uri"), + provider.to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + + assert_eq!( + handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic),), + Some(Vec::new()) + ); + } + + #[test] + fn bare_import_path_does_not_suppress_a_namespace_import() { + let main = "import * as deep from lib.math.deep;\nfunction main() returns (word) { return math.value(); }\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document( + Url::parse("file:///main/math/deep.solc").expect("deep uri"), + "function old() returns (word) { return 0; }\nexport { old };\n".to_owned() + )); + assert!(world.open_document( + Url::parse("file:///main/other/math.solc").expect("candidate uri"), + "function value() returns (word) { return 1; }\nexport { value };\n".to_owned() + )); + let diagnostic = undefined_name_diagnostic(&world, &main_uri); + let insertion = main.find('\n').expect("import line end") as u32 + 1; + let expected_range = world + .line_index(&main_uri) + .expect("line index") + .range(insertion, insertion); + + let actions = handle_code_action(&world, &main_uri, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + let action = action(&actions); + + assert_eq!(action.title, "Import module `math` from `lib.other.math`"); + assert_eq!( + action + .edit + .as_ref() + .and_then(|edit| edit.changes.as_ref()) + .and_then(|changes| changes.get(&main_uri)), + Some(&vec![TextEdit { + range: expected_range, + new_text: "import * as math from lib.other.math;\n".to_owned(), + }]) + ); + } + + #[test] + fn auto_import_candidates_stay_inside_the_current_workspace_root() { + on_windows_sized_stack(auto_import_candidates_stay_inside_the_current_workspace_root_inner); + } + + fn auto_import_candidates_stay_inside_the_current_workspace_root_inner() { + let base = std::env::temp_dir().join("solcore-lsp-auto-import-roots"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root"); + let right_root = Url::from_directory_path(&right_path).expect("right root"); + let left_main = Url::from_file_path(left_path.join("main.solc")).expect("left main"); + let left_math = Url::from_file_path(left_path.join("math.solc")).expect("left math"); + let right_extra = Url::from_file_path(right_path.join("extra.solc")).expect("right extra"); + let main = "function main() returns (word) { return value(); }\n"; + let provider = "function value() returns (word) { return 1; }\nexport { value };\n"; + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root, + vec![ + (left_main.clone(), main.to_owned()), + (left_math, provider.to_owned()), + ], + ), + (right_root, vec![(right_extra, provider.to_owned())]), + ]); + assert!(world.open_document(left_main.clone(), main.to_owned())); + let diagnostic = undefined_name_diagnostic(&world, &left_main); + + let actions = + handle_code_action(&world, &left_main, diagnostic.range, &context(diagnostic)) + .expect("code actions"); + + assert_eq!(action(&actions).title, "Import `value` from `lib.math`"); + } +} diff --git a/crates/lsp/src/completion.rs b/crates/lsp/src/completion.rs new file mode 100644 index 00000000..2c59a078 --- /dev/null +++ b/crates/lsp/src/completion.rs @@ -0,0 +1,976 @@ +//! Completion support over the wasm-clean LSP core. + +use hir::{ + anchor::DefId, + ast::{ + function::{FuncParam, PatKind, StmtKind}, + item::{ContractItem, ContractKind, Item, Module}, + }, + input::SourceFile, + nameres::{ + BuiltinKind, DefResolutionKind, ImportedNames, LocalBinding, Namespace, Resolution, + ScopeEntry, TypeVarBinding, + }, + span::{Span, Spanned, SpannedElem}, +}; +use lsp_types::{CompletionItem, CompletionItemKind, CompletionResponse, Position, Url}; + +use crate::{ + resolve::{function_owning_offset, module_id_for_uri}, + state::WorldState, +}; + +const KEYWORDS: &[&str] = &[ + "contract", + "interface", + "library", + "import", + "from", + "export", + "as", + "let", + "comptime", + "alias", + "enum", + "struct", + "trait", + "impl", + "where", + "if", + "else", + "for", + "while", + "switch", + "type", + "is", + "case", + "default", + "match", + "public", + "external", + "internal", + "private", + "payable", + "pure", + "view", + "memory", + "storage", + "calldata", + "function", + "returns", + "constructor", + "fallback", + "return", + "revert", + "leave", + "continue", + "break", + "unchecked", + "lam", + "assembly", + "pragma", + "true", + "false", +]; + +/// Computes completion items at a source position. +pub fn handle_completion( + world: &WorldState, + uri: &Url, + position: Position, +) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + let scope = hir::nameres::item_scope_facts(db, module); + let owner = function_owning_offset(db, module, file, offset); + let enclosing_contract = owner + .as_ref() + .and_then(|owner| owner.enclosing_contract) + .or_else(|| contract_at_offset(db, module, file, offset)); + + if let Some(context) = qualified_completion_context(line_index.text(), offset) { + let mut completions = CompletionAccumulator::default(); + add_qualified_completions( + db, + &scope, + enclosing_contract, + &env, + &context, + &mut completions, + ); + return Some(CompletionResponse::Array(completions.finish())); + } + + let mut completions = CompletionAccumulator::default(); + add_keyword_completions(&mut completions); + add_item_scope_completions(db, &scope, enclosing_contract, &mut completions); + add_imported_completions(db, &env, &mut completions); + + if let Some(owner) = owner { + add_body_completions( + db, + module, + &scope, + file, + offset, + owner.function, + owner.root_body, + owner.enclosing_contract, + owner.inherited_type_vars, + &env, + &mut completions, + ); + } + + Some(CompletionResponse::Array(completions.finish())) +} + +#[derive(Debug, PartialEq, Eq)] +struct QualifiedCompletionContext { + qualifier: String, + member_prefix: String, +} + +fn qualified_completion_context(text: &str, offset: u32) -> Option { + let before_cursor = text.get(..usize::try_from(offset).ok()?)?; + let path_start = before_cursor + .char_indices() + .rev() + .find_map(|(index, ch)| (!is_qualified_path_char(ch)).then_some(index + ch.len_utf8())) + .unwrap_or(0); + let path = &before_cursor[path_start..]; + let dot = path.rfind('.')?; + let qualifier = &path[..dot]; + let member_prefix = &path[dot + 1..]; + + if qualifier.is_empty() + || qualifier.split('.').any(|segment| !is_identifier(segment)) + || (!member_prefix.is_empty() && !is_identifier(member_prefix)) + { + return None; + } + + Some(QualifiedCompletionContext { + qualifier: qualifier.to_owned(), + member_prefix: member_prefix.to_owned(), + }) +} + +fn is_qualified_path_char(ch: char) -> bool { + ch == '.' || ch == '-' || ch == '_' || ch.is_alphanumeric() +} + +fn is_identifier(text: &str) -> bool { + text.split('-').all(is_identifier_chunk) +} + +fn is_identifier_chunk(text: &str) -> bool { + let mut chars = text.chars(); + chars.next().is_some_and(char::is_alphabetic) + && chars.all(|ch| ch == '_' || ch.is_alphanumeric()) +} + +fn add_qualified_completions<'db>( + db: &'db vfs::AnalysisHost, + scope: &hir::nameres::ItemScopeFacts<'db>, + enclosing_contract: Option>, + imports: &dyn ImportedNames<'db>, + context: &QualifiedCompletionContext, + completions: &mut CompletionAccumulator, +) { + let contract = enclosing_contract.and_then(|contract| scope.contract_scope(contract)); + add_qualified_scope_entries(db, &scope.terms, contract, context, completions); + add_qualified_scope_entries(db, &scope.types, None, context, completions); + add_qualified_scope_entries(db, &scope.modules, None, context, completions); + + if let Some(contract) = contract { + add_qualified_scope_entries(db, &contract.terms, None, context, completions); + add_qualified_scope_entries(db, &contract.types, None, context, completions); + } + + for namespace in [Namespace::Term, Namespace::Type, Namespace::Module] { + for name in imports.candidate_names(db, namespace) { + let Some(member) = direct_qualified_member(&name, context) else { + continue; + }; + let resolution = imports.imported(db, namespace, &name); + let kind = resolution.as_ref().map_or_else( + || completion_kind_for_namespace(namespace), + |resolution| completion_kind_for_resolution(db, resolution), + ); + let detail = resolution.as_ref().map_or_else( + || detail_for_namespace(namespace), + |resolution| detail_for_resolution(db, resolution), + ); + completions.push(member.to_owned(), kind, Some(detail)); + } + } +} + +fn add_qualified_scope_entries( + db: &vfs::AnalysisHost, + entries: &hir::nameres::NamespaceTable<'_>, + enclosing_contract: Option<&hir::nameres::ContractScope<'_>>, + context: &QualifiedCompletionContext, + completions: &mut CompletionAccumulator, +) { + for entry in entries { + if enclosing_contract + .is_some_and(|contract| contract.is_external_function_resolution(&entry.resolution)) + { + continue; + } + let Some(member) = direct_qualified_member(&entry.name, context) else { + continue; + }; + completions.push( + member.to_owned(), + completion_kind_for_resolution(db, &entry.resolution), + Some(detail_for_resolution(db, &entry.resolution)), + ); + } +} + +fn direct_qualified_member<'a>( + name: &'a str, + context: &QualifiedCompletionContext, +) -> Option<&'a str> { + let rest = name.strip_prefix(&context.qualifier)?.strip_prefix('.')?; + (!rest.is_empty() && !rest.contains('.') && rest.starts_with(&context.member_prefix)) + .then_some(rest) +} + +fn contract_at_offset<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + module.items(db).iter().find_map(|item| { + let Item::ContractDef(contract) = *item else { + return None; + }; + contract.items(db).iter().find_map(|item| { + let ContractItem::FunctionDef(function) = *item else { + return None; + }; + let body = function.body(db)?; + let span = body.span(db).resolve_to_absolute(db); + (span.file() == file + && span.start().as_u32() <= offset + && offset <= span.end().as_u32()) + .then_some(contract.def_id_value(db)) + }) + }) +} + +#[derive(Default)] +struct CompletionAccumulator { + items: Vec, +} + +impl CompletionAccumulator { + fn push(&mut self, label: String, kind: CompletionItemKind, detail: Option<&'static str>) { + if self + .items + .iter() + .any(|item| item.label == label && item.kind == Some(kind)) + { + return; + } + + self.items.push(CompletionItem { + label, + kind: Some(kind), + detail: detail.map(str::to_owned), + ..CompletionItem::default() + }); + } + + fn finish(mut self) -> Vec { + self.items.sort_by(|left, right| { + left.label + .cmp(&right.label) + .then_with(|| kind_rank(left.kind).cmp(&kind_rank(right.kind))) + }); + self.items + } +} + +fn add_keyword_completions(completions: &mut CompletionAccumulator) { + for keyword in KEYWORDS { + completions.push( + (*keyword).to_owned(), + CompletionItemKind::KEYWORD, + Some("keyword"), + ); + } +} + +fn add_item_scope_completions<'db>( + db: &'db vfs::AnalysisHost, + scope: &hir::nameres::ItemScopeFacts<'db>, + enclosing_contract: Option>, + completions: &mut CompletionAccumulator, +) { + let contract = enclosing_contract.and_then(|contract| scope.contract_scope(contract)); + for entry in &scope.terms { + if contract + .is_some_and(|contract| contract.is_external_function_resolution(&entry.resolution)) + { + continue; + } + add_scope_entry_completion(db, entry, completions); + } + for entry in &scope.types { + add_scope_entry_completion(db, entry, completions); + } + for entry in &scope.modules { + add_scope_entry_completion(db, entry, completions); + } +} + +fn add_scope_entry_completion( + db: &vfs::AnalysisHost, + entry: &ScopeEntry<'_>, + completions: &mut CompletionAccumulator, +) { + completions.push( + entry.name.clone(), + completion_kind_for_resolution(db, &entry.resolution), + Some(detail_for_resolution(db, &entry.resolution)), + ); +} + +fn add_imported_completions<'db>( + db: &'db vfs::AnalysisHost, + imports: &dyn ImportedNames<'db>, + completions: &mut CompletionAccumulator, +) { + for namespace in [Namespace::Term, Namespace::Type, Namespace::Module] { + let mut names = imports.candidate_names(db, namespace); + names.sort(); + names.dedup(); + for name in names { + let resolution = imports.imported(db, namespace, &name); + let kind = resolution.as_ref().map_or_else( + || completion_kind_for_namespace(namespace), + |resolution| completion_kind_for_resolution(db, resolution), + ); + let detail = resolution.as_ref().map_or_else( + || detail_for_namespace(namespace), + |resolution| detail_for_resolution(db, resolution), + ); + completions.push(name, kind, Some(detail)); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn add_body_completions<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + scope: &hir::nameres::ItemScopeFacts<'db>, + file: SourceFile, + offset: u32, + function: hir::ast::item::FunctionDef<'db>, + root_body: hir::ast::function::FuncBody<'db>, + enclosing_contract: Option>, + mut type_vars: Vec>, + imports: &dyn ImportedNames<'db>, + completions: &mut CompletionAccumulator, +) { + let sig = function.sig(db); + type_vars.extend(hir::nameres::type_var_bindings( + function.def_id_value(db), + &sig.type_vars, + )); + + for param in sig.params.atom() { + if let Some(name) = param_name(param) { + completions.push( + name.atom().text(db).to_owned(), + CompletionItemKind::VARIABLE, + Some("parameter"), + ); + } + } + for type_var in &type_vars { + completions.push( + type_var.name.atom().text(db).to_owned(), + CompletionItemKind::TYPE_PARAMETER, + Some("type parameter"), + ); + } + if let Some(contract) = enclosing_contract.and_then(|contract| scope.contract_scope(contract)) { + for entry in &contract.terms { + if contract.is_unqualified_term_visible(entry) { + add_scope_entry_completion(db, entry, completions); + } + } + for entry in &contract.types { + add_scope_entry_completion(db, entry, completions); + } + for field in &contract.fields { + completions.push(field.name.clone(), CompletionItemKind::FIELD, Some("field")); + } + } + + let context = hir::nameres::BodyResolutionContext { + module, + enclosing_contract, + params: hir::nameres::param_bindings(sig.params.atom()), + type_vars, + }; + let body_map = hir::nameres::resolve_body_with_imports_and_policy( + db, + root_body, + &context, + imports, + hir::nameres::NameresDiagnosticPolicy::Emit, + ); + + for binding in &body_map.stmt_bindings { + let Resolution::Local(LocalBinding::Let { .. }) = &binding.resolution else { + continue; + }; + let stmt = binding.body.stmts(db).get(binding.stmt); + if let StmtKind::Let { name, .. } = &stmt.kind { + add_local_if_visible(db, file, offset, name, completions); + } + } + for binding in &body_map.pats { + let Resolution::Local(LocalBinding::Pattern { .. }) = &binding.resolution else { + continue; + }; + let pat = binding.body.pats(db).get(binding.pat); + if let PatKind::Var(name) = &pat.kind { + add_local_if_visible(db, file, offset, name, completions); + } + } +} + +fn param_name<'a, 'db>( + param: &'a FuncParam<'db>, +) -> Option<&'a SpannedElem<'db, hir::ast::Ident<'db>>> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => Some(name), + FuncParam::Error { .. } => None, + } +} + +fn add_local_if_visible<'db>( + db: &'db vfs::AnalysisHost, + file: SourceFile, + offset: u32, + name: &SpannedElem<'db, hir::ast::Ident<'db>>, + completions: &mut CompletionAccumulator, +) { + if span_ends_before_offset(db, name.span(db), file, offset) { + completions.push( + name.atom().text(db).to_owned(), + CompletionItemKind::VARIABLE, + Some("local"), + ); + } +} + +fn span_ends_before_offset<'db>( + db: &'db vfs::AnalysisHost, + span: Span<'db>, + file: SourceFile, + offset: u32, +) -> bool { + let absolute = span.resolve_to_absolute(db); + absolute.file() == file && absolute.end().as_u32() <= offset +} + +fn completion_kind_for_namespace(namespace: Namespace) -> CompletionItemKind { + match namespace { + Namespace::Type => CompletionItemKind::STRUCT, + Namespace::Term => CompletionItemKind::VALUE, + Namespace::Field => CompletionItemKind::FIELD, + Namespace::Module => CompletionItemKind::MODULE, + } +} + +fn detail_for_namespace(namespace: Namespace) -> &'static str { + match namespace { + Namespace::Type => "type", + Namespace::Term => "term", + Namespace::Field => "field", + Namespace::Module => "module", + } +} + +fn completion_kind_for_resolution( + db: &vfs::AnalysisHost, + resolution: &Resolution<'_>, +) -> CompletionItemKind { + match resolution { + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } => CompletionItemKind::FUNCTION, + Resolution::Def { + kind: DefResolutionKind::Contract, + def, + } => match contract_kind_for_def(db, *def) { + Some(ContractKind::Interface) => CompletionItemKind::INTERFACE, + Some(ContractKind::Library) => CompletionItemKind::MODULE, + Some(ContractKind::Contract) | None => CompletionItemKind::CLASS, + }, + Resolution::Def { + kind: DefResolutionKind::Adt, + .. + } => CompletionItemKind::ENUM, + Resolution::Def { + kind: DefResolutionKind::TypeAlias | DefResolutionKind::ValueType, + .. + } => CompletionItemKind::STRUCT, + Resolution::Def { + kind: DefResolutionKind::Class, + .. + } => CompletionItemKind::INTERFACE, + Resolution::Def { + kind: DefResolutionKind::Instance, + .. + } => CompletionItemKind::CLASS, + Resolution::Ctor { .. } => CompletionItemKind::CONSTRUCTOR, + Resolution::Local(LocalBinding::TypeVar(_)) => CompletionItemKind::TYPE_PARAMETER, + Resolution::Local(_) | Resolution::Param(_) => CompletionItemKind::VARIABLE, + Resolution::Field(_) => CompletionItemKind::FIELD, + Resolution::ClassMethod { .. } => CompletionItemKind::METHOD, + Resolution::Module(_) => CompletionItemKind::MODULE, + Resolution::Builtin(BuiltinKind::Type(_)) => CompletionItemKind::STRUCT, + Resolution::Builtin(BuiltinKind::Class(_)) => CompletionItemKind::INTERFACE, + Resolution::Builtin(BuiltinKind::Constructor(_)) => CompletionItemKind::CONSTRUCTOR, + Resolution::Builtin(BuiltinKind::Function(_)) => CompletionItemKind::FUNCTION, + Resolution::Builtin(BuiltinKind::ClassMethod(_)) => CompletionItemKind::METHOD, + Resolution::DotCtorDeferred | Resolution::Err => CompletionItemKind::TEXT, + } +} + +fn detail_for_resolution(db: &vfs::AnalysisHost, resolution: &Resolution<'_>) -> &'static str { + match resolution { + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } => "function", + Resolution::Def { + kind: DefResolutionKind::Contract, + def, + } => contract_kind_for_def(db, *def) + .unwrap_or(ContractKind::Contract) + .keyword(), + Resolution::Def { + kind: DefResolutionKind::Adt, + .. + } => "enum", + Resolution::Def { + kind: DefResolutionKind::TypeAlias, + .. + } => "type alias", + Resolution::Def { + kind: DefResolutionKind::ValueType, + .. + } => "user-defined value type", + Resolution::Def { + kind: DefResolutionKind::Class, + .. + } => "trait", + Resolution::Def { + kind: DefResolutionKind::Instance, + .. + } => "impl", + Resolution::Ctor { .. } => "constructor", + Resolution::Local(LocalBinding::TypeVar(_)) => "type parameter", + Resolution::Local(_) => "local", + Resolution::Param(_) => "parameter", + Resolution::Field(_) => "field", + Resolution::ClassMethod { .. } => "trait method", + Resolution::Module(_) => "module", + Resolution::Builtin(BuiltinKind::Type(_)) => "builtin type", + Resolution::Builtin(BuiltinKind::Class(_)) => "builtin trait", + Resolution::Builtin(BuiltinKind::Constructor(_)) => "builtin constructor", + Resolution::Builtin(BuiltinKind::Function(_)) => "builtin function", + Resolution::Builtin(BuiltinKind::ClassMethod(_)) => "builtin trait method", + Resolution::DotCtorDeferred => "constructor", + Resolution::Err => "unresolved", + } +} + +fn contract_kind_for_def(db: &vfs::AnalysisHost, def: DefId<'_>) -> Option { + let module = parser::parse_file_to_hir(db, def.file(db)).module(db); + module.items(db).iter().find_map(|item| match item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(contract.kind(db)), + _ => None, + }) +} + +fn kind_rank(kind: Option) -> u8 { + let Some(kind) = kind else { + return u8::MAX; + }; + if kind == CompletionItemKind::KEYWORD { + 0 + } else if kind == CompletionItemKind::FUNCTION { + 1 + } else if kind == CompletionItemKind::METHOD { + 2 + } else if kind == CompletionItemKind::CONSTRUCTOR { + 3 + } else if kind == CompletionItemKind::VARIABLE { + 4 + } else if kind == CompletionItemKind::FIELD { + 5 + } else if kind == CompletionItemKind::ENUM { + 6 + } else if kind == CompletionItemKind::STRUCT { + 7 + } else if kind == CompletionItemKind::CLASS { + 8 + } else if kind == CompletionItemKind::INTERFACE { + 9 + } else if kind == CompletionItemKind::MODULE { + 10 + } else if kind == CompletionItemKind::TYPE_PARAMETER { + 11 + } else { + 100 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn function_body_completion_includes_params_locals_and_top_level_items() { + let source = "\ +function helper() returns (word) { + return 1; +} + +function main(input: word) returns (word) { + let local = input; + return local; +} +"; + let (world, uri) = world_with_main(source); + let offset = (source.find("return local").expect("return local") + "return ".len()) as u32; + let position = world + .line_index(&uri) + .expect("line index") + .byte_to_position(offset); + + let items = + completion_items(handle_completion(&world, &uri, position).expect("completion")); + + assert_completion(&items, "input", CompletionItemKind::VARIABLE); + assert_completion(&items, "local", CompletionItemKind::VARIABLE); + assert_completion(&items, "helper", CompletionItemKind::FUNCTION); + } + + #[test] + fn completion_includes_language_keywords() { + let source = "function main() returns (word) {\n return 1;\n}\n"; + let (world, uri) = world_with_main(source); + let offset = source.find('1').expect("literal") as u32; + let position = world + .line_index(&uri) + .expect("line index") + .byte_to_position(offset); + + let items = + completion_items(handle_completion(&world, &uri, position).expect("completion")); + + assert_completion(&items, "function", CompletionItemKind::KEYWORD); + assert_completion(&items, "returns", CompletionItemKind::KEYWORD); + assert_completion(&items, "trait", CompletionItemKind::KEYWORD); + assert_completion(&items, "is", CompletionItemKind::KEYWORD); + assert_completion(&items, "storage", CompletionItemKind::KEYWORD); + assert_completion(&items, "while", CompletionItemKind::KEYWORD); + assert_no_completion(&items, "forall"); + } + + #[test] + fn completion_preserves_contract_like_shell_kinds() { + let source = "\ +interface Reader { + function read(key: word) external view returns (word); +} + +library Helpers { + function identity(value: word) internal pure returns (word) { return value; } +} + +function main() returns (word) { + return 1; +} +"; + let (world, uri) = world_with_main(source); + let offset = source.rfind('1').expect("literal") as u32; + let position = world + .line_index(&uri) + .expect("line index") + .byte_to_position(offset); + let items = + completion_items(handle_completion(&world, &uri, position).expect("completion")); + + let reader = items + .iter() + .find(|item| item.label == "Reader") + .expect("interface completion"); + assert_eq!(reader.kind, Some(CompletionItemKind::INTERFACE)); + assert_eq!(reader.detail.as_deref(), Some("interface")); + + let helpers = items + .iter() + .find(|item| item.label == "Helpers") + .expect("library completion"); + assert_eq!(helpers.kind, Some(CompletionItemKind::MODULE)); + assert_eq!(helpers.detail.as_deref(), Some("library")); + } + + #[test] + fn completion_uses_requested_module_when_unrelated_document_opened_first() { + let unrelated = "function unrelated() returns (word) { return 0; }\n"; + let math = "function combine(a: word, b: word) returns (word) { return a + b; }\n\nexport { combine };\n"; + let main = "import {combine} from math;\n\nfunction main() returns (word) {\n return combine(1, 2);\n}\n"; + let unrelated_uri = Url::parse("file:///main/unrelated.solc").expect("unrelated uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let mut world = WorldState::new(); + assert!(world.open_document(unrelated_uri, unrelated.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let offset = (main.find("return combine").expect("return call") + "return ".len()) as u32; + let position = world + .line_index(&main_uri) + .expect("line index") + .byte_to_position(offset); + + let items = completion_items( + handle_completion(&world, &main_uri, position).expect("completion response"), + ); + + assert_completion(&items, "combine", CompletionItemKind::FUNCTION); + } + + #[test] + fn trailing_dot_module_completion_is_member_only_and_respects_exports() { + let math = "\ +function visible() returns (word) { return 1; } +function hidden() returns (word) { return 2; } +enum Color { Red, Green } +export { visible, Color(Red, Green) }; +"; + let main = "\ +import * as math from math; +function main() returns (word) { + return math.; +} +"; + let (world, main_uri) = world_with_module(main, "math.solc", math); + let items = completion_at(&world, &main_uri, main, "math."); + + assert_completion(&items, "visible", CompletionItemKind::FUNCTION); + assert_completion(&items, "Color", CompletionItemKind::ENUM); + assert_no_completion(&items, "hidden"); + assert_no_completion(&items, "Red"); + assert_no_completion(&items, "function"); + assert!( + items.iter().all(|item| !item.label.contains('.')), + "expected direct member labels only, got {items:#?}" + ); + } + + #[test] + fn qualified_completion_filters_a_typed_member_prefix() { + let math = "\ +function visible() returns (word) { return 1; } +function value() returns (word) { return 2; } +export { visible, value }; +"; + let main = "\ +import * as math from math; +function main() returns (word) { + return math.vis; +} +"; + let (world, main_uri) = world_with_module(main, "math.solc", math); + let items = completion_at(&world, &main_uri, main, "math.vis"); + + assert_completion(&items, "visible", CompletionItemKind::FUNCTION); + assert_no_completion(&items, "value"); + } + + #[test] + fn qualified_completion_includes_contract_local_adt_constructors() { + let source = "\ +contract Palette { + enum Color { Red, Green } + + function main() returns (word) { + return Color.; + } +} +"; + let (world, uri) = world_with_main(source); + let items = completion_at(&world, &uri, source, "Color."); + + assert_completion(&items, "Red", CompletionItemKind::CONSTRUCTOR); + assert_completion(&items, "Green", CompletionItemKind::CONSTRUCTOR); + assert_no_completion(&items, "Color.Red"); + } + + #[test] + fn completion_hides_only_the_current_librarys_external_functions() { + let source = "\ +library Other { + function available() external returns (word) { return 1; } +} + +library Helpers { + function external_value() external returns (word) { return 2; } + function internal_value() internal returns (word) { return 3; } + + function qualified_probe() internal returns (word) { + return Helpers.; + } + + function other_library_probe() internal returns (word) { + return Other.; + } + + function item_scope_probe() internal returns (word) { + return missing; + } +} +"; + let (world, uri) = world_with_main(source); + + let own_members = completion_at(&world, &uri, source, "return Helpers."); + assert_no_completion(&own_members, "external_value"); + assert_completion(&own_members, "internal_value", CompletionItemKind::FUNCTION); + + let other_members = completion_at(&world, &uri, source, "return Other."); + assert_completion(&other_members, "available", CompletionItemKind::FUNCTION); + + let offset = + (source.find("return missing").expect("item completion") + "return ".len()) as u32; + let position = world + .line_index(&uri) + .expect("line index") + .byte_to_position(offset); + let items = + completion_items(handle_completion(&world, &uri, position).expect("completion")); + assert_no_completion(&items, "external_value"); + assert_no_completion(&items, "Helpers.external_value"); + assert_completion(&items, "internal_value", CompletionItemKind::FUNCTION); + } + + #[test] + fn qualified_completion_includes_imported_trait_methods() { + let classes = "\ +trait Eq { + function eq(x: a, y: a) returns (bool); + function unequal(x: a, y: a) returns (bool); +} +export { Eq }; +"; + let main = "\ +import {Eq} from classes; +function main() returns (word) { + return Eq.; +} +"; + let (world, main_uri) = world_with_module(main, "classes.solc", classes); + let items = completion_at(&world, &main_uri, main, "Eq."); + + assert_completion(&items, "eq", CompletionItemKind::METHOD); + assert_completion(&items, "unequal", CompletionItemKind::METHOD); + assert_no_completion(&items, "Eq.eq"); + } + + #[test] + fn qualified_context_accepts_only_lexer_shaped_hyphenated_identifiers() { + let valid = "foo-bar.member-prefix"; + assert_eq!( + qualified_completion_context(valid, valid.len() as u32), + Some(QualifiedCompletionContext { + qualifier: "foo-bar".to_owned(), + member_prefix: "member-prefix".to_owned(), + }) + ); + + for invalid in ["foo-.member", "foo--bar.member", "foo.-member"] { + assert_eq!( + qualified_completion_context(invalid, invalid.len() as u32), + None, + "unexpectedly accepted {invalid:?}" + ); + } + } + + fn world_with_module(main: &str, module_path: &str, module_source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let module_uri = Url::parse(&format!("file:///main/{module_path}")).expect("module uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + assert!(world.open_document(module_uri, module_source.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + (world, main_uri) + } + + fn completion_at( + world: &WorldState, + uri: &Url, + source: &str, + cursor_after: &str, + ) -> Vec { + let offset = + (source.find(cursor_after).expect("completion marker") + cursor_after.len()) as u32; + let position = world + .line_index(uri) + .expect("line index") + .byte_to_position(offset); + completion_items(handle_completion(world, uri, position).expect("completion response")) + } + + fn completion_items(response: CompletionResponse) -> Vec { + match response { + CompletionResponse::Array(items) => items, + CompletionResponse::List(list) => list.items, + } + } + + fn assert_completion(items: &[CompletionItem], label: &str, kind: CompletionItemKind) { + assert!( + items + .iter() + .any(|item| item.label == label && item.kind == Some(kind)), + "expected completion {label:?} with kind {kind:?}, got {items:#?}" + ); + } + + fn assert_no_completion(items: &[CompletionItem], label: &str) { + assert!( + items.iter().all(|item| item.label != label), + "did not expect completion {label:?}, got {items:#?}" + ); + } +} diff --git a/crates/lsp/src/definition.rs b/crates/lsp/src/definition.rs new file mode 100644 index 00000000..e7e6a10e --- /dev/null +++ b/crates/lsp/src/definition.rs @@ -0,0 +1,714 @@ +//! Go-to-definition support over the wasm-clean LSP core. + +use hir::{ + anchor::{DefId, resolve_def_location}, + ast::{ + function::{ExprKind, FuncBody, FuncParam, PatKind, StmtKind}, + item::{AdtDef, ClassDef, ContractDef, ContractItem, FunctionDef, Item, Module}, + }, + diag::{AbsoluteSpan, Offset}, + nameres::{self as hir_nameres, FieldId, LocalBinding, ParamId, Resolution, TypeVarBinding}, + span::{Span, Spanned}, +}; +use lsp_types::{GotoDefinitionResponse, Location, Position, Url}; + +use crate::{ + references::{import_export_target_at, reference_target_at, target_declaration_span}, + resolve::{function_owning_offset, innermost_expr, module_id_for_uri}, + state::WorldState, +}; + +/// Computes the target definition location for the symbol at a source position. +pub fn handle_definition( + world: &WorldState, + uri: &Url, + position: Position, +) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + + if let Some(location) = (|| { + let owner = function_owning_offset(db, module, file, offset)?; + let body_map = body_resolution_map( + db, + module, + owner.function, + owner.root_body, + owner.enclosing_contract, + owner.inherited_type_vars, + &env, + ); + let (owning_body, expr_id) = innermost_expr(db, owner.root_body, file, offset)?; + let resolution = body_map + .exprs + .iter() + .find(|entry| entry.body == owning_body && entry.expr == expr_id)? + .resolution + .clone(); + let target = resolution_target_span(db, module, &env, resolution)?; + location_for_span(world, db, target) + })() { + return Some(GotoDefinitionResponse::Scalar(location)); + } + + if let Some(location) = + import_module_location_at(world, db, module, current_module, file, offset) + { + return Some(GotoDefinitionResponse::Scalar(location)); + } + + if let Some(location) = (|| { + let target = reference_target_at(world, uri, position)?; + let span = target_declaration_span(db, &target)?; + location_for_span(world, db, span) + })() { + return Some(GotoDefinitionResponse::Scalar(location)); + } + + let target = import_export_target_at(world, uri, position)?; + let span = target_declaration_span(db, &target)?; + let location = location_for_span(world, db, span)?; + + Some(GotoDefinitionResponse::Scalar(location)) +} + +fn body_resolution_map<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + mut type_vars: Vec>, + imports: &dyn hir_nameres::ImportedNames<'db>, +) -> hir_nameres::BodyResolutionMap<'db> { + let sig = function.sig(db); + type_vars.extend(hir_nameres::type_var_bindings( + function.def_id_value(db), + &sig.type_vars, + )); + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract, + params: hir_nameres::param_bindings(sig.params.atom()), + type_vars, + }; + hir_nameres::resolve_body_with_imports_and_policy( + db, + root_body, + &context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ) +} + +fn resolution_target_span<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + imports: &nameres::ModuleEnv<'db>, + resolution: Resolution<'db>, +) -> Option { + match resolution { + Resolution::Def { def, .. } => def_name_span(db, def), + Resolution::Ctor { ty, index } => ctor_name_span(db, ty, index.as_usize()), + Resolution::Param(param) => param_name_span(db, module, param), + Resolution::Local(LocalBinding::Let { body, stmt }) => { + let stmt = body.stmts(db).get(stmt); + let span = match &stmt.kind { + StmtKind::Let { name, .. } => name.span(db), + _ => stmt.span, + }; + Some(span.resolve_to_absolute(db)) + } + Resolution::Local(LocalBinding::Pattern { body, pat }) => { + let pat = body.pats(db).get(pat); + let span = match &pat.kind { + PatKind::Var(name) => name.span(db), + _ => pat.span, + }; + Some(span.resolve_to_absolute(db)) + } + Resolution::Local(LocalBinding::TypeVar(_)) => None, + Resolution::Field(field) => field_name_span(db, field), + Resolution::ClassMethod { class, name } => class_method_name_span(db, class, &name), + Resolution::Module(module_ref) => imports + .modules + .get(&module_ref.name) + .copied() + .or_else(|| { + imports + .module_origins + .get(&module_ref.name) + .copied() + .flatten() + }) + .and_then(|module| module_start_span(db, module)), + Resolution::DotCtorDeferred | Resolution::Builtin(_) | Resolution::Err => None, + } +} + +fn import_module_location_at<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + file: hir::input::SourceFile, + offset: u32, +) -> Option { + for item in module.items(db) { + let Item::Import(import) = *item else { + continue; + }; + let on_path = import + .path_elems(db) + .iter() + .any(|segment| span_contains_offset(db, segment.span(db), file, offset)); + let on_alias = import + .alias_elem(db) + .is_some_and(|alias| span_contains_offset(db, alias.span(db), file, offset)); + if !on_path && !on_alias { + continue; + } + + let target = nameres::resolve_direct_import_target(db, current_module, import).ok()?; + let span = module_start_span(db, target)?; + return location_for_span(world, db, span); + } + + None +} + +fn module_start_span<'db>( + db: &'db dyn hir_ty::Db, + module: nameres::ModuleId<'db>, +) -> Option { + let file = db.module_file(module)?; + Some(AbsoluteSpan::new(file, Offset::new(0), Offset::new(0))) +} + +fn span_contains_offset<'db>( + db: &'db dyn hir_ty::Db, + span: Span<'db>, + file: hir::input::SourceFile, + offset: u32, +) -> bool { + let absolute = span.resolve_to_absolute(db); + absolute.file() == file + && absolute.start().as_u32() <= offset + && offset < absolute.end().as_u32() +} + +fn def_name_span<'db>(db: &'db dyn hir_ty::Db, def: DefId<'db>) -> Option { + let file = def.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_def_name_span_in_module(db, module, def) + .map(|span| span.resolve_to_absolute(db)) + .or_else(|| { + let location = resolve_def_location(db.def_location_table(file), def)?; + Some(AbsoluteSpan::new( + location.file, + location.base_offset, + location.base_offset, + )) + }) +} + +fn find_def_name_span_in_module<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + Item::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + Item::ClassDef(class) if class.def_id_value(db) == def => { + return Some(class.head(db).kind(db).class.span(db)); + } + Item::InstanceDef(instance) if instance.def_id_value(db) == def => { + return Some(instance.head(db).span(db)); + } + Item::ContractDef(contract) => { + if contract.def_id_value(db) == def { + return Some(contract.name_elem(db).span(db)); + } + if let Some(span) = find_def_name_span_in_contract(db, contract, def) { + return Some(span); + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn find_def_name_span_in_contract<'db>( + db: &'db dyn hir_ty::Db, + contract: ContractDef<'db>, + def: DefId<'db>, +) -> Option> { + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + ContractItem::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + + None +} + +fn ctor_name_span<'db>( + db: &'db dyn hir_ty::Db, + ty: DefId<'db>, + index: usize, +) -> Option { + let file = ty.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_adt(db, module, ty)? + .ctors(db) + .get(index) + .map(|ctor| ctor.name.span(db).resolve_to_absolute(db)) +} + +fn find_adt<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match *item { + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + _ => None, + }), + _ => None, + }) +} + +fn param_name_span<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + param: ParamId<'db>, +) -> Option { + find_param_span_in_module(db, module, param.body, param.index.as_usize()) + .map(|span| span.resolve_to_absolute(db)) +} + +fn find_param_span_in_module<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + if let Some(span) = find_param_span_in_function(db, function, body, index) { + return Some(span); + } + } + Item::ContractDef(contract) => { + for contract_item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *contract_item + && let Some(span) = find_param_span_in_function(db, function, body, index) + { + return Some(span); + } + } + } + Item::InstanceDef(instance) => { + for function in instance.methods(db) { + if let Some(span) = find_param_span_in_function(db, *function, body, index) { + return Some(span); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn find_param_span_in_function<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + if function.body(db) == Some(body) { + return function + .sig(db) + .params + .atom() + .get(index) + .and_then(|param| param_name_or_whole_span(db, param)); + } + + find_lambda_param_span(db, function.body(db)?, body, index) +} + +fn find_lambda_param_span<'db>( + db: &'db dyn hir_ty::Db, + root: FuncBody<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + let mut stack = vec![root]; + while let Some(current) = stack.pop() { + for (_, expr) in current.exprs(db).iter() { + if let ExprKind::Lambda { + params, + body: lambda_body, + .. + } = &expr.kind + { + if *lambda_body == body { + return params + .atom() + .get(index) + .and_then(|param| param_name_or_whole_span(db, param)); + } + stack.push(*lambda_body); + } + } + } + + None +} + +fn param_name_or_whole_span<'db>( + db: &'db dyn hir_ty::Db, + param: &FuncParam<'db>, +) -> Option> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => Some(name.span(db)), + FuncParam::Error { span } if !span.resolve_to_absolute(db).is_empty() => Some(*span), + FuncParam::Error { .. } => None, + } +} + +fn field_name_span<'db>(db: &'db dyn hir_ty::Db, field: FieldId<'db>) -> Option { + let file = field.contract.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_contract(db, module, field.contract)? + .fields(db) + .get(field.index.as_usize()) + .map(|field| field.name().span(db).resolve_to_absolute(db)) +} + +fn class_method_name_span<'db>( + db: &'db dyn hir_ty::Db, + class: DefId<'db>, + name: &str, +) -> Option { + let file = class.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_class(db, module, class)? + .methods(db) + .iter() + .find(|method| method.name.atom().text(db) == name) + .map(|method| method.name.span(db).resolve_to_absolute(db)) +} + +fn find_contract<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(contract), + _ => None, + }) +} + +fn find_class<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::ClassDef(class) if class.def_id_value(db) == def => Some(class), + _ => None, + }) +} + +fn location_for_span( + world: &WorldState, + db: &vfs::AnalysisHost, + span: AbsoluteSpan, +) -> Option { + let uri = world.client_uri_for_vfs_url(span.file().url(db).as_str())?; + let line_index = world.line_index(&uri)?; + let range = line_index.range(span.start().as_u32(), span.end().as_u32()); + + Some(Location { uri, range }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn world_with_main_and_math(main: &str, math: &str) -> (WorldState, Url, Url) { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math.to_owned())); + (world, main_uri, math_uri) + } + + fn world_with_main_and_nested( + main: &str, + nested_path: &str, + nested: &str, + ) -> (WorldState, Url, Url) { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let nested_uri = + Url::parse(&format!("file:///main/{nested_path}.solc")).expect("nested uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(nested_uri.clone(), nested.to_owned())); + (world, main_uri, nested_uri) + } + + fn scalar_definition(world: &WorldState, uri: &Url, offset: u32) -> Location { + let line_index = world.line_index(uri).expect("line index"); + let response = + handle_definition(world, uri, line_index.byte_to_position(offset)).expect("definition"); + let GotoDefinitionResponse::Scalar(location) = response else { + panic!("expected scalar definition response"); + }; + location + } + + #[test] + fn definition_of_parameter_use_points_to_parameter_name() { + let source = "function id(x: word) returns (word) {\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let use_offset = (source.find("return x").expect("return") + "return ".len()) as u32; + let param_offset = source.find("x: word").expect("param") as u32; + let line_index = world.line_index(&uri).expect("line index"); + let position = line_index.byte_to_position(use_offset); + + let response = handle_definition(&world, &uri, position).expect("definition"); + let GotoDefinitionResponse::Scalar(location) = response else { + panic!("expected scalar definition response"); + }; + + assert_eq!(location.uri, uri); + assert_eq!( + location.range, + line_index.range(param_offset, param_offset + 1) + ); + } + + #[test] + fn definition_of_import_selector_name_points_to_imported_declaration() { + let main = + "import {double} from math;\nfunction main() returns (word) { return double(21); }\n"; + let math = + "function double(x: word) returns (word) { return x + x; }\nexport { double };\n"; + let (world, main_uri, math_uri) = world_with_main_and_math(main, math); + let main_index = world.line_index(&main_uri).expect("main line index"); + let math_index = world.line_index(&math_uri).expect("math line index"); + let import = main.find("double").expect("import") as u32; + let declaration = math.find("double").expect("declaration") as u32; + + let response = handle_definition(&world, &main_uri, main_index.byte_to_position(import)) + .expect("definition"); + let GotoDefinitionResponse::Scalar(location) = response else { + panic!("expected scalar definition response"); + }; + + assert_eq!(location.uri, math_uri); + assert_eq!( + location.range, + math_index.range(declaration, declaration + "double".len() as u32) + ); + } + + #[test] + fn definition_in_embedded_std_is_not_returned_as_an_unopenable_uri() { + let source = "import {addWord} from std;\nfunction main() returns (word) { return addWord(1, 2); }\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let call = source.rfind("addWord").expect("call") as u32; + + assert_eq!( + handle_definition(&world, &uri, line_index.byte_to_position(call)), + None + ); + } + + #[test] + fn definition_of_cross_file_type_ref_points_to_type_declaration() { + let main = "\ +import {Box} from models; +function wrap(value: word) returns (Box) { + let boxed: Box = Box.Box(value); + return boxed; +} +"; + let models = "enum Box { Box(word) }\nexport { Box(*) };\n"; + let (world, main_uri, models_uri) = world_with_main_and_nested(main, "models", models); + let models_index = world.line_index(&models_uri).expect("models line index"); + let type_ref = (main.find("boxed: Box").expect("local type") + "boxed: ".len()) as u32; + let declaration = models.find("Box").expect("type declaration") as u32; + + let location = scalar_definition(&world, &main_uri, type_ref); + + assert_eq!(location.uri, models_uri); + assert_eq!( + location.range, + models_index.range(declaration, declaration + "Box".len() as u32) + ); + } + + #[test] + fn definition_on_type_declaration_points_to_itself() { + let source = "enum Choice { Left, Right }\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let declaration = source.find("Choice").expect("declaration") as u32; + + let location = scalar_definition(&world, &uri, declaration); + + assert_eq!(location.uri, uri); + assert_eq!( + location.range, + line_index.range(declaration, declaration + "Choice".len() as u32) + ); + } + + #[test] + fn definition_of_predicate_points_to_class_declaration() { + let source = "\ +trait Comparable { + function compare(x: a, y: a) returns (word); +} + +function keep(x: a) returns (a) where a: Comparable { return x; } +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let declaration = source.find("Comparable").expect("trait declaration") as u32; + let predicate = source.rfind("Comparable").expect("predicate") as u32; + + let location = scalar_definition(&world, &uri, predicate); + + assert_eq!(location.uri, uri); + assert_eq!( + location.range, + line_index.range(declaration, declaration + "Comparable".len() as u32) + ); + } + + #[test] + fn definition_of_constructor_pattern_points_to_constructor_declaration() { + let source = "\ +enum Choice { Left(word), Right } + +function unwrap(value: Choice) returns (word) { + match (value) { case Choice.Left(x) { return x; } case Choice.Right { return 0; } } +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let declaration = source.find("Left").expect("constructor declaration") as u32; + let pattern = source.rfind("Left").expect("constructor pattern") as u32; + + let location = scalar_definition(&world, &uri, pattern); + + assert_eq!(location.uri, uri); + assert_eq!( + location.range, + line_index.range(declaration, declaration + "Left".len() as u32) + ); + } + + #[test] + fn definition_of_import_path_and_module_qualifier_points_to_module_start() { + let main = "import * as bar from foo.bar;\nfunction main() returns (word) { return bar.value(); }\n"; + let bar = "export { value };\nfunction value() returns (word) { return 7; }\n"; + let (world, main_uri, bar_uri) = world_with_main_and_nested(main, "foo/bar", bar); + let bar_index = world.line_index(&bar_uri).expect("bar line index"); + let expected = bar_index.range(0, 0); + let import_foo = main.find("foo.bar").expect("import path") as u32; + let import_bar = import_foo + "foo.".len() as u32; + let alias_bar = main.find("bar from").expect("module alias") as u32; + let qualifier_bar = main.rfind("bar.value").expect("module qualifier") as u32; + + for offset in [import_foo, import_bar, alias_bar, qualifier_bar] { + let location = scalar_definition(&world, &main_uri, offset); + assert_eq!(location.uri, bar_uri, "offset {offset}"); + assert_eq!(location.range, expected, "offset {offset}"); + } + } + + #[test] + fn definition_of_exact_module_qualifier_wins_over_shared_navigation_origin() { + let main = "import * as bar from foo.bar;\nimport * as foo from foo;\nfunction main() returns (word) { return foo.value(); }\n"; + let foo = "export { value };\nfunction value() returns (word) { return 1; }\n"; + let bar = "export { value };\nfunction value() returns (word) { return 2; }\n"; + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let foo_uri = Url::parse("file:///main/foo.solc").expect("foo uri"); + let bar_uri = Url::parse("file:///main/foo/bar.solc").expect("bar uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(foo_uri.clone(), foo.to_owned())); + assert!(world.open_document(bar_uri, bar.to_owned())); + let foo_index = world.line_index(&foo_uri).expect("foo line index"); + let qualifier = main.rfind("foo.value").expect("exact module qualifier") as u32; + + let location = scalar_definition(&world, &main_uri, qualifier); + + assert_eq!(location.uri, foo_uri); + assert_eq!(location.range, foo_index.range(0, 0)); + } +} diff --git a/crates/lsp/src/diagnostics.rs b/crates/lsp/src/diagnostics.rs new file mode 100644 index 00000000..c1bc65ae --- /dev/null +++ b/crates/lsp/src/diagnostics.rs @@ -0,0 +1,444 @@ +//! Diagnostics conversion from `solcore-vfs` to LSP diagnostics. +//! +//! VFS diagnostics carry byte ranges in source-file URL strings. This module +//! filters them to the requested publish URI and maps primary ranges through +//! the open document's UTF-16 line index. + +use lsp_types::{ + Diagnostic as LspDiagnostic, DiagnosticRelatedInformation, + DiagnosticSeverity as LspDiagnosticSeverity, Location, NumberOrString, Url, +}; +use nameres::Db as _; +use vfs::{ + DiagLabel, DiagRange, Diagnostic as VfsDiagnostic, DiagnosticSeverity as VfsDiagnosticSeverity, +}; + +use crate::{analysis::with_analysis_stack, line_index::LineIndexExt, state::WorldState}; + +/// Computes LSP diagnostics for a single open document URI. +pub fn compute_diagnostics(world: &WorldState, uri: &Url) -> Vec { + let Some(line_index) = world.line_index(uri) else { + return Vec::new(); + }; + + compute_vfs_diagnostics(world, uri) + .into_iter() + .map(|diagnostic| to_lsp_diagnostic(world, line_index, diagnostic)) + .collect() +} + +/// Computes compiler diagnostics belonging to one client document while +/// retaining structured suggestions for code-action conversion. +pub(crate) fn compute_vfs_diagnostics(world: &WorldState, uri: &Url) -> Vec { + with_analysis_stack(|| compute_vfs_diagnostics_inner(world, uri)) +} + +fn compute_vfs_diagnostics_inner(world: &WorldState, uri: &Url) -> Vec { + let Some(path) = world.vfs_path_for_uri(uri) else { + return Vec::new(); + }; + + let diagnostics = if is_reachable_from_workspace_entry(world, &path) { + world.workspace().diagnostics() + } else { + world.workspace().diagnostics_for_entry(&path) + }; + + diagnostics + .into_iter() + .filter(|diagnostic| diagnostic_belongs_to_uri(world, diagnostic, uri)) + .collect() +} + +/// Computes diagnostics for every open document in deterministic URI order. +pub fn compute_open_document_diagnostics(world: &WorldState) -> Vec<(Url, Vec)> { + world + .open_document_uris() + .into_iter() + .map(|uri| { + let diagnostics = compute_diagnostics(world, &uri); + (uri, diagnostics) + }) + .collect() +} + +fn is_reachable_from_workspace_entry(world: &WorldState, path: &str) -> bool { + let db = world.db(); + let Some(file) = db.source_file(path) else { + return false; + }; + let Some(entry) = world.workspace().entry_module() else { + return false; + }; + + nameres::reachable_modules(db, entry) + .into_iter() + .any(|module| db.module_file(module) == Some(file)) +} + +fn diagnostic_belongs_to_uri(world: &WorldState, diagnostic: &VfsDiagnostic, uri: &Url) -> bool { + diagnostic + .primary + .as_ref() + .and_then(|primary| world.client_uri_for_vfs_url(&primary.file_url)) + .is_some_and(|primary_uri| primary_uri == *uri) +} + +pub(crate) fn to_lsp_diagnostic( + world: &WorldState, + line_index: &LineIndexExt, + diagnostic: VfsDiagnostic, +) -> LspDiagnostic { + let primary = diagnostic + .primary + .as_ref() + .expect("diagnostics are filtered to those with a primary range"); + let related_information = related_information(world, &diagnostic.labels); + let message = message_with_notes_and_helps(&diagnostic); + + LspDiagnostic { + range: line_index.range(primary.start, primary.end), + severity: Some(to_lsp_severity(diagnostic.severity)), + code: diagnostic.code.map(NumberOrString::String), + code_description: None, + source: Some("solcore".to_owned()), + message, + related_information, + tags: None, + data: None, + } +} + +fn related_information( + world: &WorldState, + labels: &[DiagLabel], +) -> Option> { + let related = labels + .iter() + .filter(|label| !label.is_primary) + .filter_map(|label| { + let message = label.message.as_ref()?; + let (uri, range) = location_for_range(world, &label.range)?; + Some(DiagnosticRelatedInformation { + location: Location::new(uri, range), + message: message.clone(), + }) + }) + .collect::>(); + + (!related.is_empty()).then_some(related) +} + +fn location_for_range(world: &WorldState, range: &DiagRange) -> Option<(Url, lsp_types::Range)> { + let uri = world.client_uri_for_vfs_url(&range.file_url)?; + let line_index = world.line_index(&uri)?; + Some((uri, line_index.range(range.start, range.end))) +} + +fn to_lsp_severity(severity: VfsDiagnosticSeverity) -> LspDiagnosticSeverity { + match severity { + VfsDiagnosticSeverity::Error => LspDiagnosticSeverity::ERROR, + VfsDiagnosticSeverity::Warning => LspDiagnosticSeverity::WARNING, + VfsDiagnosticSeverity::Note => LspDiagnosticSeverity::INFORMATION, + VfsDiagnosticSeverity::Help => LspDiagnosticSeverity::HINT, + } +} + +fn message_with_notes_and_helps(diagnostic: &VfsDiagnostic) -> String { + let mut message = diagnostic.message.clone(); + for note in &diagnostic.notes { + message.push_str("\n\nnote: "); + message.push_str(note); + } + for help in &diagnostic.helps { + message.push_str("\n\nhelp: "); + message.push_str(help); + } + message +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::WorldState; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn assert_no_module_not_found(diagnostics: &[LspDiagnostic]) { + assert!( + diagnostics.iter().all(|diagnostic| { + !diagnostic.message.contains("file not found") + && diagnostic.code + != Some(NumberOrString::String( + hir::diag::DiagnosticCode::MODULE_NOT_FOUND.to_owned(), + )) + }), + "expected no module-not-found diagnostics, got {diagnostics:#?}" + ); + } + + fn assert_no_unknown_import_item(diagnostics: &[LspDiagnostic]) { + assert!( + !has_unknown_import_item(diagnostics), + "expected no unknown-import-item diagnostics, got {diagnostics:#?}" + ); + } + + fn assert_valid_sibling_import(diagnostics: &[LspDiagnostic]) { + assert_no_module_not_found(diagnostics); + assert_no_unknown_import_item(diagnostics); + } + + fn diagnostics_for_uri<'a>( + diagnostics: &'a [(Url, Vec)], + uri: &Url, + ) -> &'a [LspDiagnostic] { + diagnostics + .iter() + .find_map(|(diagnostic_uri, diagnostics)| { + (diagnostic_uri == uri).then_some(&**diagnostics) + }) + .unwrap_or_else(|| panic!("expected diagnostics for {uri}, got {diagnostics:#?}")) + } + + #[test] + fn clean_program_has_no_diagnostics() { + let (world, uri) = world_with_main("function main() returns (word) {\n return 1;\n}\n"); + + assert!(compute_diagnostics(&world, &uri).is_empty()); + } + + #[test] + fn type_error_maps_to_lsp_error_with_range() { + let source = "function f() returns (word) {\n return true;\n}\n"; + let (world, uri) = world_with_main(source); + + let diagnostics = compute_diagnostics(&world, &uri); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.severity == Some(LspDiagnosticSeverity::ERROR)), + "expected at least one error diagnostic, got {diagnostics:#?}" + ); + assert!(diagnostics.iter().all(|diagnostic| { + diagnostic.range.start.line <= diagnostic.range.end.line + && diagnostic.range.start != diagnostic.range.end + })); + } + + #[test] + fn sibling_import_open_in_workspace_has_no_module_not_found_diagnostic() { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math = "function double(x: word) returns (word) {\n let res: word;\n assembly {\n res := add(x, x)\n }\n return res;\n}\n\nexport { double };\n"; + + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let _ = compute_diagnostics(&world, &main_uri); + assert!(world.open_document(math_uri, math.to_owned())); + + let diagnostics = compute_diagnostics(&world, &main_uri); + assert_valid_sibling_import(&diagnostics); + } + + #[test] + fn sibling_import_opened_before_importer_has_no_module_not_found_diagnostic() { + let mut world = WorldState::new(); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math = "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + + assert!(world.open_document(math_uri, math.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + + let diagnostics = compute_diagnostics(&world, &main_uri); + assert_valid_sibling_import(&diagnostics); + } + + #[test] + fn fallback_diagnostics_for_unreachable_importer_update_after_sibling_opens() { + let mut world = WorldState::new(); + let entry_uri = Url::parse("file:///main/entry.solc").expect("entry uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let entry = "function entry() returns (word) { return 0; }\n"; + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math = "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + + assert!(world.open_document(entry_uri, entry.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let diagnostics = compute_diagnostics(&world, &main_uri); + assert!( + diagnostics.iter().any(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::MODULE_NOT_FOUND.to_owned(), + )) + }), + "expected module-not-found before math opens, got {diagnostics:#?}" + ); + + assert!(world.open_document(math_uri, math.to_owned())); + let diagnostics = compute_diagnostics(&world, &main_uri); + assert_valid_sibling_import(&diagnostics); + } + + #[test] + fn fallback_diagnostics_does_not_mutate_a_cloned_salsa_database() { + use std::{sync::mpsc, time::Duration}; + + let mut world = WorldState::new(); + let entry_uri = Url::parse("file:///main/entry.solc").expect("entry uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let shadow_uri = Url::parse("file:///main/math.txt").expect("shadow uri"); + let entry = "function entry() returns (word) { return 0; }\n"; + let main = + "import {double} from math;\nfunction main() returns (word) { return double(21); }\n"; + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + + assert!(world.open_document(entry_uri, entry.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + assert!(world.open_document(shadow_uri.clone(), "notes".to_owned())); + world.close_document(&shadow_uri); + assert!(world.remove_workspace_document(&shadow_uri)); + + let (sender, receiver) = mpsc::channel(); + std::thread::spawn(move || { + let diagnostics = compute_diagnostics(&world, &main_uri); + let _ = sender.send(diagnostics); + }); + + receiver + .recv_timeout(Duration::from_secs(5)) + .expect("alternate-entry diagnostics must not wait for a Salsa clone to drop"); + } + + #[test] + fn excessive_expression_nesting_is_diagnosed_on_a_small_analysis_stack() { + let result = std::thread::Builder::new() + .stack_size(1024 * 1024) + .spawn(|| { + let mut source = "function main() returns (word) { return ".to_owned(); + source.push_str(&"true ? 0 : ".repeat(130)); + source.push_str("0; }\n"); + let (world, uri) = world_with_main(&source); + + let diagnostics = compute_diagnostics(&world, &uri); + + assert!(diagnostics.iter().any(|diagnostic| { + diagnostic + .message + .contains("nesting exceeds the compiler limit") + })); + }) + .expect("spawn small-stack analysis") + .join(); + if let Err(payload) = result { + std::panic::resume_unwind(payload); + } + } + + #[test] + fn open_document_diagnostics_refresh_importer_when_sibling_changes() { + let mut world = WorldState::new(); + let entry_uri = Url::parse("file:///main/entry.solc").expect("entry uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let entry = "function entry() returns (word) { return 0; }\n"; + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math_no_export = "function double(x: word) returns (word) { return x; }\n"; + let math_with_export = + "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + + assert!(world.open_document(entry_uri, entry.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math_no_export.to_owned())); + let stale = compute_open_document_diagnostics(&world); + assert!( + has_unknown_import_item(diagnostics_for_uri(&stale, &main_uri)), + "expected UnknownImportItem while `double` is unexported, got {stale:#?}" + ); + + assert!(world.change_document(&math_uri, math_with_export.to_owned())); + let refreshed = compute_open_document_diagnostics(&world); + assert_valid_sibling_import(diagnostics_for_uri(&refreshed, &main_uri)); + } + + fn has_unknown_import_item(diagnostics: &[LspDiagnostic]) -> bool { + diagnostics.iter().any(|diagnostic| { + diagnostic.code + == Some(NumberOrString::String( + hir::diag::DiagnosticCode::MODULE_UNKNOWN_IMPORT_ITEM.to_owned(), + )) + }) + } + + // REPRO A: math is opened WITHOUT the export (cold: `double` is genuinely + // unexported, causing UnknownImportItem), then the export is added via a content + // change. main's import resolution must re-run and clear the diagnostic. + #[test] + fn adding_export_via_change_clears_unknown_import_item() { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math_no_export = "function double(x: word) returns (word) { return x; }\n"; + let math_with_export = + "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math_no_export.to_owned())); + let cold = compute_diagnostics(&world, &main_uri); + assert!( + has_unknown_import_item(&cold), + "expected UnknownImportItem while `double` is unexported, got {cold:#?}" + ); + + assert!(world.change_document(&math_uri, math_with_export.to_owned())); + let warm = compute_diagnostics(&world, &main_uri); + assert!( + !has_unknown_import_item(&warm), + "export added: UnknownImportItem must clear, got {warm:#?}" + ); + assert_no_module_not_found(&warm); + assert_no_unknown_import_item(&warm); + } + + // REPRO B: same, but the entry drifts off the importer (a third file is the + // LSP entry) so diagnostics take the clone+set_entry fallback path. + #[test] + fn adding_export_via_change_clears_unknown_import_item_entry_drift() { + let mut world = WorldState::new(); + let entry_uri = Url::parse("file:///main/entry.solc").expect("entry uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let entry = "function entry() returns (word) { return 0; }\n"; + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math_no_export = "function double(x: word) returns (word) { return x; }\n"; + let math_with_export = + "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + + assert!(world.open_document(entry_uri, entry.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math_no_export.to_owned())); + let _cold = compute_diagnostics(&world, &main_uri); + + assert!(world.change_document(&math_uri, math_with_export.to_owned())); + let warm = compute_diagnostics(&world, &main_uri); + assert!( + !has_unknown_import_item(&warm), + "export added (entry drift): UnknownImportItem must clear, got {warm:#?}" + ); + assert_no_module_not_found(&warm); + assert_no_unknown_import_item(&warm); + } +} diff --git a/crates/lsp/src/document_highlight.rs b/crates/lsp/src/document_highlight.rs new file mode 100644 index 00000000..bef1917d --- /dev/null +++ b/crates/lsp/src/document_highlight.rs @@ -0,0 +1,96 @@ +//! Document highlight support over the wasm-clean LSP core. + +use lsp_types::{DocumentHighlight, DocumentHighlightKind, Position, Url}; + +use crate::{ + references::{collect_reference_locations, reference_target_at}, + state::WorldState, +}; + +/// Computes same-document highlights for the symbol at a source position. +pub fn handle_document_highlight( + world: &WorldState, + uri: &Url, + position: Position, +) -> Option> { + let target = reference_target_at(world, uri, position)?; + let mut highlights = collect_reference_locations(world, &target, true) + .into_iter() + .filter(|location| location.uri == *uri) + .map(|location| { + // NOTE(codex): The public references core exposes occurrence + // locations, but not declaration spans, so highlights are textual. + DocumentHighlight { + range: location.range, + kind: Some(DocumentHighlightKind::TEXT), + } + }) + .collect::>(); + + highlights.sort_by(|left, right| { + left.range + .start + .line + .cmp(&right.range.start.line) + .then_with(|| left.range.start.character.cmp(&right.range.start.character)) + .then_with(|| left.range.end.line.cmp(&right.range.end.line)) + .then_with(|| left.range.end.character.cmp(&right.range.end.character)) + }); + + Some(highlights) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn parameter_highlights_declaration_and_uses_in_current_file() { + let source = "function id(x: word) returns (word) {\n let y = x;\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let first_use = (source.find("let y = x").expect("first use") + "let y = ".len()) as u32; + let second_use = (source.find("return x").expect("second use") + "return ".len()) as u32; + let declaration = source.find("x: word").expect("declaration") as u32; + let position = line_index.byte_to_position(first_use); + + let highlights = + handle_document_highlight(&world, &uri, position).expect("document highlights"); + + assert_eq!( + highlights, + vec![ + DocumentHighlight { + range: line_index.range(declaration, declaration + 1), + kind: Some(DocumentHighlightKind::TEXT), + }, + DocumentHighlight { + range: line_index.range(first_use, first_use + 1), + kind: Some(DocumentHighlightKind::TEXT), + }, + DocumentHighlight { + range: line_index.range(second_use, second_use + 1), + kind: Some(DocumentHighlightKind::TEXT), + }, + ] + ); + } + + #[test] + fn whitespace_returns_none() { + let source = "function id(x: word) returns (word) {\n let y = x;\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let whitespace = (source.find("let y = x").expect("let statement") + "let".len()) as u32; + let position = line_index.byte_to_position(whitespace); + + assert_eq!(handle_document_highlight(&world, &uri, position), None); + } +} diff --git a/crates/lsp/src/folding.rs b/crates/lsp/src/folding.rs new file mode 100644 index 00000000..29b15414 --- /dev/null +++ b/crates/lsp/src/folding.rs @@ -0,0 +1,435 @@ +//! Folding ranges for declarations, imports, comments, and lexical blocks. +//! +//! Structural delimiter scanning intentionally remains useful while a document +//! is syntactically incomplete. Parsed HIR item spans supplement that lexical +//! view for declaration-level folds. + +use std::{cmp::Reverse, collections::HashSet}; + +use hir::{ast::item::Item, span::Spanned}; +use lsp_types::{FoldingRange, FoldingRangeKind, Position, Url}; + +use crate::{line_index::LineIndexExt, state::WorldState}; + +/// Computes deterministic folding ranges for any document known to the LSP +/// workspace. +pub fn handle_folding_range(world: &WorldState, uri: &Url) -> Option> { + let line_index = world.line_index(uri)?; + let source = line_index.text(); + let _source_len = u32::try_from(source.len()).ok()?; + let lexical = scan_source(source); + let (item_ranges, import_ranges) = hir_item_ranges(world, uri, source.len()); + + // Prefer semantically labelled ranges when line-only clients would see two + // equivalent ranges. Lexical block ranges are then used for nested blocks + // and malformed source not represented in HIR. + let mut ranges = Vec::new(); + ranges.extend(comment_folds(line_index, source, &lexical)); + ranges.extend(import_folds(line_index, &import_ranges)); + ranges.extend( + item_ranges + .iter() + .filter_map(|range| folding_range(line_index, *range, None)), + ); + ranges.extend( + lexical + .delimiters + .iter() + .filter(|delimiter| delimiter.opening == b'{') + .filter(|delimiter| { + !item_ranges.iter().any(|item| { + item.end == delimiter.range.end + && line_index.byte_to_position(item.start as u32).line + == line_index + .byte_to_position(delimiter.range.start as u32) + .line + }) + }) + .filter_map(|delimiter| folding_range(line_index, delimiter.range, None)), + ); + + let mut seen_ranges = HashSet::new(); + ranges.retain(|range| { + seen_ranges.insert(( + range.start_line, + range.start_character, + range.end_line, + range.end_character, + )) + }); + ranges.sort_by_key(|range| { + ( + range.start_line, + range.start_character.unwrap_or(0), + Reverse(range.end_line), + Reverse(range.end_character.unwrap_or(0)), + ) + }); + Some(ranges) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ByteRange { + pub(crate) start: usize, + pub(crate) end: usize, +} + +impl ByteRange { + pub(crate) fn contains_offset(self, offset: usize) -> bool { + self.start <= offset && offset <= self.end + } + + pub(crate) fn contains_range(self, other: Self) -> bool { + self.start <= other.start && other.end <= self.end + } + + pub(crate) fn len(self) -> usize { + self.end.saturating_sub(self.start) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct DelimiterRange { + pub(crate) opening: u8, + pub(crate) range: ByteRange, +} + +#[derive(Debug, Default)] +pub(crate) struct LexicalStructure { + pub(crate) delimiters: Vec, + pub(crate) block_comments: Vec, + pub(crate) line_comments: Vec, +} + +/// Scans balanced delimiters and comments without requiring a successful +/// parse. Delimiters in comments and string literals are ignored. +pub(crate) fn scan_source(source: &str) -> LexicalStructure { + let bytes = source.as_bytes(); + let mut result = LexicalStructure::default(); + let mut delimiters = Vec::<(u8, usize)>::new(); + let mut i = 0usize; + + while i < bytes.len() { + match bytes[i] { + b'"' => { + i += 1; + while i < bytes.len() { + match bytes[i] { + b'\\' => i = (i + 2).min(bytes.len()), + b'"' => { + i += 1; + break; + } + _ => i += 1, + } + } + } + b'/' if bytes.get(i + 1) == Some(&b'/') => { + let start = i; + i += 2; + while i < bytes.len() && bytes[i] != b'\n' { + i += 1; + } + result.line_comments.push(ByteRange { start, end: i }); + } + b'/' if bytes.get(i + 1) == Some(&b'*') => { + let start = i; + let mut depth = 1usize; + i += 2; + while i < bytes.len() && depth > 0 { + if bytes.get(i..i + 2) == Some(b"/*") { + depth += 1; + i += 2; + } else if bytes.get(i..i + 2) == Some(b"*/") { + depth -= 1; + i += 2; + } else { + i += 1; + } + } + result.block_comments.push(ByteRange { start, end: i }); + } + opening @ (b'{' | b'(' | b'[') => { + delimiters.push((opening, i)); + i += 1; + } + closing @ (b'}' | b')' | b']') => { + let expected = match closing { + b'}' => b'{', + b')' => b'(', + b']' => b'[', + _ => unreachable!(), + }; + if delimiters + .last() + .is_some_and(|(opening, _)| *opening == expected) + { + let (opening, start) = delimiters.pop().expect("checked delimiter"); + result.delimiters.push(DelimiterRange { + opening, + range: ByteRange { start, end: i + 1 }, + }); + } + i += 1; + } + _ => i += 1, + } + } + + result +} + +fn hir_item_ranges( + world: &WorldState, + uri: &Url, + source_len: usize, +) -> (Vec, Vec) { + let db = world.db(); + let Some(path) = world.vfs_path_for_uri(uri) else { + return (Vec::new(), Vec::new()); + }; + let Some(file) = db.source_file(&path) else { + return (Vec::new(), Vec::new()); + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let mut items = Vec::new(); + let mut imports = Vec::new(); + + for item in module.items(db) { + let absolute = item.span(db).resolve_to_absolute(db); + let range = ByteRange { + start: absolute.start().as_u32() as usize, + end: absolute.end().as_u32() as usize, + }; + if range.start > range.end || range.end > source_len { + continue; + } + if matches!(item, Item::Import(_)) { + imports.push(range); + } else { + items.push(range); + } + } + + (items, imports) +} + +fn comment_folds( + line_index: &LineIndexExt, + source: &str, + lexical: &LexicalStructure, +) -> Vec { + let mut folds = lexical + .block_comments + .iter() + .filter_map(|range| folding_range(line_index, *range, Some(FoldingRangeKind::Comment))) + .collect::>(); + + let mut line_comments = lexical + .line_comments + .iter() + .filter_map(|range| { + let position = line_index.byte_to_position(range.start as u32); + let line_start = line_start_offset(source, range.start); + source[line_start..range.start] + .chars() + .all(char::is_whitespace) + .then_some((*range, position.line)) + }) + .collect::>(); + line_comments.sort_by_key(|(_, line)| *line); + + let mut run: Option<(ByteRange, u32)> = None; + for (range, line) in line_comments { + match run { + Some((current, end_line)) if line == end_line + 1 => { + run = Some(( + ByteRange { + end: range.end, + ..current + }, + line, + )); + } + Some((current, end_line)) => { + push_line_comment_run(line_index, &mut folds, current, end_line); + run = Some((range, line)); + } + None => run = Some((range, line)), + } + } + if let Some((current, end_line)) = run { + push_line_comment_run(line_index, &mut folds, current, end_line); + } + + folds +} + +fn push_line_comment_run( + line_index: &LineIndexExt, + folds: &mut Vec, + range: ByteRange, + end_line: u32, +) { + let start_line = line_index.byte_to_position(range.start as u32).line; + if end_line > start_line + && let Some(fold) = folding_range(line_index, range, Some(FoldingRangeKind::Comment)) + { + folds.push(fold); + } +} + +fn import_folds(line_index: &LineIndexExt, imports: &[ByteRange]) -> Vec { + let mut imports = imports.to_vec(); + imports.sort_by_key(|range| range.start); + let mut groups = Vec::new(); + let mut current: Option = None; + + for import in imports { + match current { + Some(group) => { + let group_end_line = line_index.byte_to_position(group.end as u32).line; + let import_start_line = line_index.byte_to_position(import.start as u32).line; + if import_start_line <= group_end_line + 1 { + current = Some(ByteRange { + start: group.start, + end: import.end, + }); + } else { + groups.push(group); + current = Some(import); + } + } + None => current = Some(import), + } + } + if let Some(group) = current { + groups.push(group); + } + + groups + .into_iter() + .filter_map(|range| folding_range(line_index, range, Some(FoldingRangeKind::Imports))) + .collect() +} + +fn folding_range( + line_index: &LineIndexExt, + range: ByteRange, + kind: Option, +) -> Option { + let start = line_index.byte_to_position(range.start as u32); + let end = line_index.byte_to_position(range.end as u32); + (start.line < end.line).then(|| lsp_folding_range(start, end, kind)) +} + +fn lsp_folding_range( + start: Position, + end: Position, + kind: Option, +) -> FoldingRange { + FoldingRange { + start_line: start.line, + start_character: Some(start.character), + end_line: end.line, + end_character: Some(end.character), + kind, + collapsed_text: None, + } +} + +fn line_start_offset(source: &str, offset: usize) -> usize { + source[..offset] + .rfind('\n') + .map_or(0, |newline| newline + 1) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn folds_imports_comments_items_and_nested_blocks() { + let source = "// first\n// second\nimport * as alpha from alpha;\nimport * as beta from beta;\n\n/* block\n comment */\ncontract Box {\n function get() returns (word) {\n if (true) {\n return 1;\n }\n }\n}\n"; + let (world, uri) = world_with_main(source); + let folds = handle_folding_range(&world, &uri).expect("folding ranges"); + + assert!(folds.iter().any(|fold| { + fold.kind == Some(FoldingRangeKind::Comment) + && fold.start_line == 0 + && fold.end_line == 1 + })); + assert!(folds.iter().any(|fold| { + fold.kind == Some(FoldingRangeKind::Imports) + && fold.start_line == 2 + && fold.end_line == 3 + })); + assert!( + folds + .iter() + .any(|fold| fold.start_line == 7 && fold.end_line == 13) + ); + assert!( + folds + .iter() + .any(|fold| fold.start_line == 9 && fold.end_line == 11) + ); + } + + #[test] + fn lexical_folding_ignores_delimiters_in_unicode_strings_and_comments() { + let source = "function main() {\n let label = \"😀 { not a block }\";\n /* { ignored } */\n {\n return 1;\n }\n}\n"; + let (world, uri) = world_with_main(source); + let folds = handle_folding_range(&world, &uri).expect("folding ranges"); + + assert_eq!( + folds + .iter() + .filter(|fold| fold.kind.is_none()) + .map(|fold| (fold.start_line, fold.end_line)) + .collect::>(), + vec![(0, 6), (3, 5)] + ); + } + + #[test] + fn malformed_source_still_returns_balanced_inner_blocks() { + let source = "function main() {\n {\n return 1;\n }\n"; + let (world, uri) = world_with_main(source); + let folds = handle_folding_range(&world, &uri).expect("folding ranges"); + + assert!( + folds + .iter() + .any(|fold| fold.start_line == 1 && fold.end_line == 3) + ); + } + + #[test] + fn nested_blocks_with_the_same_line_extent_remain_distinct() { + let source = "function main() { if (true) {\n return 1;\n} }\n"; + let (world, uri) = world_with_main(source); + let folds = handle_folding_range(&world, &uri).expect("folding ranges"); + let structural = folds + .iter() + .filter(|fold| fold.kind.is_none() && fold.start_line == 0 && fold.end_line == 2) + .collect::>(); + + assert_eq!(structural.len(), 2); + assert_ne!(structural[0].start_character, structural[1].start_character); + } + + #[test] + fn unknown_document_has_no_folding_result() { + let world = WorldState::new(); + let uri = Url::parse("file:///main/missing.solc").expect("uri"); + assert_eq!(handle_folding_range(&world, &uri), None); + } +} diff --git a/crates/lsp/src/formatting.rs b/crates/lsp/src/formatting.rs new file mode 100644 index 00000000..bce70d7c --- /dev/null +++ b/crates/lsp/src/formatting.rs @@ -0,0 +1,397 @@ +//! Deterministic whole-document formatting for Solcore source files. +//! +//! The formatter deliberately limits itself to layout-only changes: it +//! normalizes indentation from structural braces and removes trailing +//! whitespace while preserving tokens, comments, blank lines, and line-ending +//! style. That makes it safe to run on partially written or malformed source. + +use lsp_types::{FormattingOptions, TextEdit, Url}; + +use crate::state::WorldState; + +/// Computes a single whole-document edit using the client's indentation +/// preferences. +/// +/// An unchanged document produces an empty edit list. Unknown documents and +/// documents too large for LSP's `u32` positions produce `None`. +pub fn handle_formatting( + world: &WorldState, + uri: &Url, + options: &FormattingOptions, +) -> Option> { + let text = world.document_text(uri)?; + let text_len = u32::try_from(text.len()).ok()?; + let formatted = format_document(text, options); + if formatted == text { + return Some(Vec::new()); + } + + let line_index = world.line_index(uri)?; + Some(vec![TextEdit { + range: line_index.range(0, text_len), + new_text: formatted, + }]) +} + +fn format_document(source: &str, options: &FormattingOptions) -> String { + // Leading/trailing whitespace is part of multiline string and block-comment + // contents. Until the formatter operates on token-preserving source spans, + // leave such documents unchanged rather than altering literal or comment + // payloads. + if contains_layout_sensitive_multiline_region(source) { + return source.to_owned(); + } + + let indent = if options.insert_spaces { + " ".repeat(options.tab_size.clamp(1, 16) as usize) + } else { + "\t".to_owned() + }; + let preferred_newline = if source.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + + let mut formatted = String::with_capacity(source.len()); + let mut scan = LayoutScan::default(); + let mut depth = 0usize; + let mut cursor = 0usize; + + while cursor < source.len() { + let remainder = &source[cursor..]; + let (segment, line_ending, consumed) = match remainder.find('\n') { + Some(relative_newline) => { + let line_end = cursor + relative_newline; + let (content_end, ending) = if source[..line_end].ends_with('\r') { + (line_end - 1, "\r\n") + } else { + (line_end, "\n") + }; + (&source[cursor..content_end], ending, relative_newline + 1) + } + None => (remainder, "", remainder.len()), + }; + + let content = segment.trim_start_matches([' ', '\t']); + let content = if options.trim_trailing_whitespace == Some(true) { + content.trim_end_matches([' ', '\t']) + } else { + content + }; + if !content.is_empty() { + let layout = scan.analyze_line(content); + let line_depth = depth.saturating_sub(layout.leading_closing_braces); + for _ in 0..line_depth.min(256) { + formatted.push_str(&indent); + } + formatted.push_str(content); + depth = depth + .saturating_add(layout.opening_braces) + .saturating_sub(layout.closing_braces); + } else { + // A blank line can still be part of a malformed multiline string + // or block comment. Let the scanner observe it; the client option + // determines whether existing blank-line whitespace is preserved. + let _ = scan.analyze_line(content); + if options.trim_trailing_whitespace != Some(true) { + formatted.push_str(segment); + } + } + formatted.push_str(line_ending); + cursor += consumed; + } + + if options.trim_final_newlines == Some(true) { + let had_final_newline = formatted.ends_with('\n'); + while formatted.ends_with('\n') { + formatted.pop(); + if formatted.ends_with('\r') { + formatted.pop(); + } + } + if had_final_newline || options.insert_final_newline == Some(true) { + formatted.push_str(preferred_newline); + } + } else if options.insert_final_newline == Some(true) && !formatted.ends_with('\n') { + formatted.push_str(preferred_newline); + } + + formatted +} + +fn contains_layout_sensitive_multiline_region(source: &str) -> bool { + let bytes = source.as_bytes(); + let mut block_comment_depth = 0usize; + let mut in_line_comment = false; + let mut in_string = false; + let mut escaped = false; + let mut i = 0usize; + + while i < bytes.len() { + if in_line_comment { + if bytes[i] == b'\n' { + in_line_comment = false; + } + i += 1; + continue; + } + if block_comment_depth > 0 { + if bytes[i] == b'\n' { + return true; + } + if bytes.get(i..i + 2) == Some(b"/*") { + block_comment_depth += 1; + i += 2; + } else if bytes.get(i..i + 2) == Some(b"*/") { + block_comment_depth -= 1; + i += 2; + } else { + i += 1; + } + continue; + } + if in_string { + if bytes[i] == b'\n' { + return true; + } + if escaped { + escaped = false; + } else if bytes[i] == b'\\' { + escaped = true; + } else if bytes[i] == b'"' { + in_string = false; + } + i += 1; + continue; + } + + match bytes[i] { + b'/' if bytes.get(i + 1) == Some(&b'/') => { + in_line_comment = true; + i += 2; + } + b'/' if bytes.get(i + 1) == Some(&b'*') => { + block_comment_depth = 1; + i += 2; + } + b'"' => { + in_string = true; + i += 1; + } + _ => i += 1, + } + } + + in_string || block_comment_depth > 0 +} + +#[derive(Debug, Default)] +struct LayoutScan { + block_comment_depth: usize, + in_string: bool, + escaped: bool, +} + +#[derive(Debug, Default)] +struct LineLayout { + opening_braces: usize, + closing_braces: usize, + leading_closing_braces: usize, +} + +impl LayoutScan { + fn analyze_line(&mut self, line: &str) -> LineLayout { + let bytes = line.as_bytes(); + let mut layout = LineLayout::default(); + let mut saw_code = self.block_comment_depth > 0 || self.in_string; + let mut i = 0usize; + + while i < bytes.len() { + if self.block_comment_depth > 0 { + if bytes.get(i..i + 2) == Some(b"/*") { + self.block_comment_depth += 1; + i += 2; + } else if bytes.get(i..i + 2) == Some(b"*/") { + self.block_comment_depth -= 1; + i += 2; + } else { + i += 1; + } + continue; + } + + if self.in_string { + if self.escaped { + self.escaped = false; + } else if bytes[i] == b'\\' { + self.escaped = true; + } else if bytes[i] == b'"' { + self.in_string = false; + } + i += 1; + continue; + } + + match bytes[i] { + b'/' if bytes.get(i + 1) == Some(&b'/') => break, + b'/' if bytes.get(i + 1) == Some(&b'*') => { + saw_code = true; + self.block_comment_depth = 1; + i += 2; + } + b'"' => { + saw_code = true; + self.in_string = true; + i += 1; + } + b'{' => { + saw_code = true; + layout.opening_braces += 1; + i += 1; + } + b'}' => { + if !saw_code { + layout.leading_closing_braces += 1; + } + layout.closing_braces += 1; + i += 1; + } + byte if byte.is_ascii_whitespace() => i += 1, + _ => { + saw_code = true; + i += 1; + } + } + } + + layout + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn options(tab_size: u32, insert_spaces: bool) -> FormattingOptions { + FormattingOptions { + tab_size, + insert_spaces, + insert_final_newline: Some(true), + trim_final_newlines: Some(true), + trim_trailing_whitespace: Some(true), + ..FormattingOptions::default() + } + } + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn formats_whole_document_without_touching_braces_in_trivia() { + let source = "function main() returns (word) { \nreturn \"{\";\n/* } { */\nif (true) {\nreturn 1; // }\n}\n}\n\n"; + let expected = "function main() returns (word) {\n return \"{\";\n /* } { */\n if (true) {\n return 1; // }\n }\n}\n"; + let (world, uri) = world_with_main(source); + + let edits = handle_formatting(&world, &uri, &options(2, true)).expect("formatting"); + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].new_text, expected); + assert_eq!( + edits[0].range.end, + world + .line_index(&uri) + .unwrap() + .byte_to_position(source.len() as u32) + ); + } + + #[test] + fn respects_tabs_and_preserves_crlf() { + let source = "function main() {\r\nreturn \"😀\";\r\n}"; + let expected = "function main() {\r\n\treturn \"😀\";\r\n}\r\n"; + let (world, uri) = world_with_main(source); + + let edits = handle_formatting(&world, &uri, &options(8, false)).expect("formatting"); + assert_eq!(edits[0].new_text, expected); + assert_eq!( + edits[0].range.end, + world + .line_index(&uri) + .unwrap() + .byte_to_position(source.len() as u32) + ); + } + + #[test] + fn already_formatted_document_needs_no_edit() { + let source = "function main() {\n return 1;\n}\n"; + let (world, uri) = world_with_main(source); + + assert_eq!( + handle_formatting(&world, &uri, &options(2, true)), + Some(Vec::new()) + ); + } + + #[test] + fn formatting_requires_an_open_document() { + let world = WorldState::new(); + let uri = Url::parse("file:///main/missing.solc").expect("uri"); + assert_eq!(handle_formatting(&world, &uri, &options(2, true)), None); + } + + #[test] + fn preserves_multiline_string_and_block_comment_payload_whitespace() { + for source in [ + "function main() {\nreturn \"first\n second \";\n}\n", + "function main() {\n/* markdown\n indented code \n*/\nreturn 1;\n}\n", + ] { + let (world, uri) = world_with_main(source); + assert_eq!( + handle_formatting(&world, &uri, &options(2, true)), + Some(Vec::new()), + "layout-sensitive payload must remain byte-for-byte unchanged" + ); + } + } + + #[test] + fn preserves_unterminated_string_and_block_comment_payload_at_eof() { + for source in [ + "function main() {\n let text = \"café ", + "function main() {\n /* markdown ", + ] { + let (world, uri) = world_with_main(source); + assert_eq!( + handle_formatting(&world, &uri, &options(2, true)), + Some(Vec::new()) + ); + } + } + + #[test] + fn honors_disabled_trailing_whitespace_trimming() { + let source = "function main() { \n \nreturn 1; \n}\n"; + let expected = "function main() { \n \n return 1; \n}\n"; + let (world, uri) = world_with_main(source); + let mut options = options(2, true); + options.trim_trailing_whitespace = Some(false); + + let edits = handle_formatting(&world, &uri, &options).expect("formatting"); + assert_eq!(edits[0].new_text, expected); + } + + #[test] + fn dedents_adjacent_leading_closing_braces() { + let source = "function main() {\n{\nreturn 1;\n }}\n"; + let expected = "function main() {\n {\n return 1;\n}}\n"; + let (world, uri) = world_with_main(source); + + let edits = handle_formatting(&world, &uri, &options(2, true)).expect("formatting"); + assert_eq!(edits[0].new_text, expected); + } +} diff --git a/crates/lsp/src/hover.rs b/crates/lsp/src/hover.rs new file mode 100644 index 00000000..b964878c --- /dev/null +++ b/crates/lsp/src/hover.rs @@ -0,0 +1,1591 @@ +//! Hover support over the wasm-clean LSP core. + +use crate::{ + references::{ReferenceTarget, reference_target_at}, + resolve::{function_owning_offset, innermost_expr, module_id_for_uri}, + state::WorldState, +}; +use hir::{ + anchor::DefId, + ast::{ + function::{ExprKind, FuncBody, FuncParam, FuncSig, PatKind, StmtKind}, + item::{ + AdtDef, ClassDef, ContractDef, ContractItem, FuncKind, FunctionDef, InstanceDef, Item, + Module, SourceComment, TypeAlias, TypeAliasKind, + }, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + nameres::{LocalBinding, ParamId, Resolution, TypeVarBinding}, + span::SpannedElem, +}; +use hir_ty::{ClassId, InferResultExt, InferenceResult, PredKind, Ty, TyCtor, TyKind, TyScheme}; +use lsp_types::{Hover, HoverContents, MarkedString, Position, Range, Url}; + +/// Computes hover information at a source position. +/// +/// Named declarations and references are resolved through the same semantic +/// identity as references/rename. Expression inference remains the fallback +/// for literals and other non-name syntax. +pub fn handle_hover(world: &WorldState, uri: &Url, position: Position) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + + if let Some(target) = reference_target_at(world, uri, position) + && let Some(info) = semantic_hover_info(db, module, current_module, &env, &target) + { + let range = identifier_range(line_index.text(), offset) + .map(|(start, end)| line_index.range(start, end)); + return Some(hover_from_info(info, range)); + } + + expression_hover(db, module, current_module, &env, file, offset, line_index) +} + +struct HoverInfo { + code: String, + documentation: Option, +} + +fn hover_from_info(info: HoverInfo, range: Option) -> Hover { + let code = MarkedString::from_language_code("solcore".to_owned(), info.code); + let contents = match info.documentation { + Some(documentation) => { + HoverContents::Array(vec![code, MarkedString::String(documentation)]) + } + None => HoverContents::Scalar(code), + }; + Hover { contents, range } +} + +fn semantic_hover_info<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + imports: &dyn hir::nameres::ImportedNames<'db>, + target: &ReferenceTarget<'db>, +) -> Option { + match target { + ReferenceTarget::Def(def) => definition_hover(db, *def), + ReferenceTarget::Ctor { ty, index } => constructor_hover(db, *ty, *index), + ReferenceTarget::Param(param) => { + parameter_hover(db, module, current_module, imports, *param) + } + ReferenceTarget::Local(local) => local_hover(db, module, current_module, imports, local), + ReferenceTarget::Field(field) => field_hover(db, *field), + ReferenceTarget::ClassMethod { class, name } => class_method_hover(db, *class, name), + ReferenceTarget::Module(module_ref) => Some(HoverInfo { + code: format!("module {}", module_ref.name), + documentation: None, + }), + ReferenceTarget::ImportAlias { name, .. } => Some(HoverInfo { + code: format!("import alias {name}"), + documentation: None, + }), + ReferenceTarget::ExportedModuleAlias { name, .. } => Some(HoverInfo { + code: format!("exported module alias {name}"), + documentation: None, + }), + } +} + +fn expression_hover<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + imports: &dyn hir::nameres::ImportedNames<'db>, + file: hir::input::SourceFile, + offset: u32, + line_index: &crate::LineIndexExt, +) -> Option { + let owner = function_owning_offset(db, module, file, offset)?; + let analysis = infer_function( + db, + module, + current_module, + imports, + &FunctionOwner { + function: owner.function, + root_body: owner.root_body, + enclosing_contract: owner.enclosing_contract, + inherited_type_vars: owner.inherited_type_vars, + }, + ); + let (owning_body, expr_id) = innermost_expr(db, owner.root_body, file, offset)?; + let ty = analysis.inference.expr_ty(owning_body, expr_id)?; + let expr = owning_body.exprs(db).get(expr_id); + let absolute = expr.span.resolve_to_absolute(db); + + Some(hover_from_info( + HoverInfo { + code: display_ty(db, ty, &analysis.type_var_names), + documentation: None, + }, + Some(line_index.range(absolute.start().as_u32(), absolute.end().as_u32())), + )) +} + +fn identifier_range(text: &str, offset: u32) -> Option<(u32, u32)> { + let offset = usize::try_from(offset).ok()?; + let suffix = text.get(offset..)?; + let (cursor, current) = match suffix.chars().next() { + Some(ch) if is_identifier_char(ch) => (offset, ch), + _ => text + .get(..offset)? + .char_indices() + .next_back() + .filter(|(_, ch)| is_identifier_char(*ch))?, + }; + + let mut start = cursor; + for (index, ch) in text.get(..cursor)?.char_indices().rev() { + if !is_identifier_char(ch) { + break; + } + start = index; + } + + let first_end = cursor + current.len_utf8(); + let mut end = first_end; + for (relative, ch) in text.get(first_end..)?.char_indices() { + if !is_identifier_char(ch) { + break; + } + end = first_end + relative + ch.len_utf8(); + } + Some((start as u32, end as u32)) +} + +fn is_identifier_char(ch: char) -> bool { + ch.is_alphanumeric() || ch == '_' || ch == '-' +} + +enum Definition<'db> { + Function(FoundFunction<'db>), + TypeAlias(TypeAlias<'db>), + Adt(FoundAdt<'db>), + Class(ClassDef<'db>), + Instance(InstanceDef<'db>), + Contract(ContractDef<'db>), +} + +struct FoundFunction<'db> { + function: FunctionDef<'db>, + type_var_names: Vec, +} + +struct FoundAdt<'db> { + adt: AdtDef<'db>, + type_var_names: Vec, +} + +fn definition_hover<'db>(db: &'db vfs::AnalysisHost, def: DefId<'db>) -> Option { + let module = parser::parse_file_to_hir(db, def.file(db)).module(db); + match find_definition(db, module, def)? { + Definition::Function(found) => { + let function = found.function; + let sig = function.sig(db); + let kind = function.kind(db); + let callable = match kind { + FuncKind::Function => { + let module_id = nameres::module_id_for_source_file(db, def.file(db)); + let scheme = + module_id.and_then(|module| hir_ty::function_scheme(db, module, def)); + scheme.map_or_else( + || format_source_function_signature(db, sig), + |scheme| { + format_callable_scheme( + db, + sig.name.atom().text(db), + &function_param_names(db, sig), + &found.type_var_names, + scheme, + Some(sig), + None, + ) + }, + ) + } + FuncKind::Constructor | FuncKind::Fallback => { + format_source_function_signature(db, sig) + } + }; + let code = match kind { + FuncKind::Function => format!("function {callable}"), + FuncKind::Constructor | FuncKind::Fallback => callable, + }; + Some(HoverInfo { + code, + documentation: comments_markdown(function.leading_comments(db)), + }) + } + Definition::TypeAlias(alias) => { + let name = alias.name_elem(db).atom().text(db); + let params = type_parameter_list(db, alias.ty_param_elems(db)); + let (keyword, separator) = match alias.kind(db) { + TypeAliasKind::Transparent => ("alias", "="), + TypeAliasKind::ValueType => ("type", "is"), + }; + Some(HoverInfo { + code: format!( + "{keyword} {name}{params} {separator} {}", + display_type_ref(db, alias.ty(db)) + ), + documentation: comments_markdown(alias.leading_comments(db)), + }) + } + Definition::Adt(found) => Some(HoverInfo { + code: format_adt_declaration(db, found.adt), + documentation: comments_markdown(found.adt.leading_comments(db)), + }), + Definition::Class(class) => { + let mut code = format!("trait {}", display_trait_ref(db, class.head(db))); + append_where_clause(db, &mut code, class.super_preds(db)); + Some(HoverInfo { + code, + documentation: comments_markdown(class.leading_comments(db)), + }) + } + Definition::Instance(instance) => { + let mut code = String::new(); + if instance.default_kw(db).is_some() { + code.push_str("default "); + } + code.push_str("impl"); + code.push_str(&type_parameter_list(db, instance.type_var_elems(db))); + code.push(' '); + code.push_str(&display_trait_ref(db, instance.head(db))); + append_where_clause(db, &mut code, instance.preds(db)); + Some(HoverInfo { + code, + documentation: comments_markdown(instance.leading_comments(db)), + }) + } + Definition::Contract(contract) => { + let name = contract.name_elem(db).atom().text(db); + Some(HoverInfo { + code: format!( + "{} {name}{}", + contract.kind(db).keyword(), + type_parameter_list(db, contract.ty_param_elems(db)) + ), + documentation: comments_markdown(contract.leading_comments(db)), + }) + } + } +} + +fn find_definition<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(Definition::Function(FoundFunction { + function, + type_var_names: function_type_var_names(db, &[], function), + })); + } + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(Definition::TypeAlias(alias)); + } + Item::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(Definition::Adt(FoundAdt { + adt, + type_var_names: ident_names(db, adt.ty_param_elems(db)), + })); + } + Item::ClassDef(class) if class.def_id_value(db) == def => { + return Some(Definition::Class(class)); + } + Item::InstanceDef(instance) => { + if instance.def_id_value(db) == def { + return Some(Definition::Instance(instance)); + } + let inherited = ident_names(db, instance.type_var_elems(db)); + if let Some(function) = instance + .methods(db) + .iter() + .copied() + .find(|function| function.def_id_value(db) == def) + { + return Some(Definition::Function(FoundFunction { + function, + type_var_names: function_type_var_names(db, &inherited, function), + })); + } + } + Item::ContractDef(contract) => { + if contract.def_id_value(db) == def { + return Some(Definition::Contract(contract)); + } + let inherited = ident_names(db, contract.ty_param_elems(db)); + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(Definition::Function(FoundFunction { + function, + type_var_names: function_type_var_names(db, &inherited, function), + })); + } + ContractItem::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(Definition::TypeAlias(alias)); + } + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => { + let mut type_var_names = inherited.clone(); + type_var_names.extend(ident_names(db, adt.ty_param_elems(db))); + return Some(Definition::Adt(FoundAdt { + adt, + type_var_names, + })); + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + None +} + +fn function_type_var_names<'db>( + db: &'db dyn hir_ty::Db, + inherited: &[String], + function: FunctionDef<'db>, +) -> Vec { + let mut names = inherited.to_vec(); + names.extend(ident_names(db, &function.sig(db).type_vars)); + names +} + +fn ident_names<'db>( + db: &'db dyn hir_ty::Db, + idents: &[SpannedElem<'db, hir::ast::Ident<'db>>], +) -> Vec { + idents + .iter() + .map(|ident| ident.atom().text(db).to_owned()) + .collect() +} + +fn function_param_names<'db>(db: &'db dyn hir_ty::Db, sig: &FuncSig<'db>) -> Vec { + sig.params + .atom() + .iter() + .filter_map(|param| match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some(name.atom().text(db).to_owned()) + } + FuncParam::Error { .. } => None, + }) + .collect() +} + +fn format_source_function_signature<'db>(db: &'db dyn hir_ty::Db, sig: &FuncSig<'db>) -> String { + let params = sig + .params + .atom() + .iter() + .map(|param| format_source_param(db, param)) + .collect::>() + .join(", "); + let mut signature = sig.name.atom().text(db).to_owned(); + signature.push_str(&type_parameter_list(db, &sig.type_vars)); + signature.push('('); + signature.push_str(¶ms); + signature.push(')'); + if let Some(visibility) = sig.visibility_kind() { + signature.push(' '); + signature.push_str(visibility.keyword()); + } + if let Some(mutability) = sig.mutability_kind() { + signature.push(' '); + signature.push_str(mutability.keyword()); + } + if let Some(ret) = sig.ret { + signature.push_str(&display_type_ref_return_suffix(db, ret)); + } + append_where_clause(db, &mut signature, &sig.preds); + signature +} + +fn format_source_param<'db>(db: &'db dyn hir_ty::Db, param: &FuncParam<'db>) -> String { + match param { + FuncParam::Typed { comptime, name, ty } => { + let prefix = if comptime.is_some() { "comptime " } else { "" }; + format!( + "{prefix}{}: {}", + name.atom().text(db), + display_type_ref(db, *ty) + ) + } + FuncParam::Untyped { comptime, name } => { + let prefix = if comptime.is_some() { "comptime " } else { "" }; + format!("{prefix}{}: _", name.atom().text(db)) + } + FuncParam::Error { .. } => ": ".to_owned(), + } +} + +fn format_adt_declaration<'db>(db: &'db dyn hir_ty::Db, adt: AdtDef<'db>) -> String { + let name = adt.name_elem(db).atom().text(db); + let params = type_parameter_list(db, adt.ty_param_elems(db)); + let ctors = adt + .ctors(db) + .iter() + .map(|ctor| { + let name = ctor.name.atom().text(db); + if ctor.field_count == 0 { + name.to_owned() + } else { + let fields = display_type_ref(db, *ctor.fields.atom()); + if ctor.field_count == 1 { + format!("{name}({fields})") + } else { + format!("{name}{fields}") + } + } + }) + .collect::>() + .join(", "); + if ctors.is_empty() { + format!("enum {name}{params} {{}}") + } else { + format!("enum {name}{params} {{ {ctors} }}") + } +} + +fn type_parameter_list<'db>( + db: &'db dyn hir_ty::Db, + params: &[SpannedElem<'db, hir::ast::Ident<'db>>], +) -> String { + if params.is_empty() { + String::new() + } else { + format!("<{}>", ident_names(db, params).join(", ")) + } +} + +fn comments_markdown(comments: &[SourceComment]) -> Option { + let text = comments + .iter() + .map(|comment| { + comment + .text + .lines() + .map(|line| line.trim().trim_start_matches('*').trim_start()) + .collect::>() + .join("\n") + }) + .collect::>() + .join("\n"); + (!text.trim().is_empty()).then_some(text) +} + +fn constructor_hover<'db>( + db: &'db vfs::AnalysisHost, + ty: DefId<'db>, + index: hir::nameres::CtorIndex, +) -> Option { + let module = parser::parse_file_to_hir(db, ty.file(db)).module(db); + let Definition::Adt(found) = find_definition(db, module, ty)? else { + return None; + }; + let ctor = found.adt.ctors(db).get(index.as_usize())?; + let name = ctor.name.atom().text(db); + let module_id = nameres::module_id_for_source_file(db, ty.file(db))?; + let scheme = hir_ty::infer::adt_ctor_scheme(db, module_id, ty, index)?; + let source_params = adt_ctor_source_params(db, ctor); + Some(HoverInfo { + code: format!( + "constructor {}", + format_callable_scheme( + db, + name, + &[], + &found.type_var_names, + scheme, + None, + Some(&source_params), + ) + ), + documentation: comments_markdown(found.adt.ctor_leading_comments(db, index.as_usize())?), + }) +} + +fn adt_ctor_source_params<'db>( + db: &'db dyn hir_ty::Db, + ctor: &hir::ast::item::AdtCtor<'db>, +) -> Vec> { + let fields = *ctor.fields.atom(); + match ctor.field_count { + 0 => Vec::new(), + 1 => vec![fields], + _ => match fields.kind(db) { + TypeRefKind::Tuple { elems } => elems.atom().clone(), + _ => vec![fields], + }, + } +} + +fn field_hover<'db>( + db: &'db vfs::AnalysisHost, + field: hir::nameres::FieldId<'db>, +) -> Option { + let module = parser::parse_file_to_hir(db, field.contract.file(db)).module(db); + let Definition::Contract(contract) = find_definition(db, module, field.contract)? else { + return None; + }; + let field_def = contract.fields(db).get(field.index.as_usize())?; + Some(HoverInfo { + code: format!( + "{}: {}", + field_def.name().atom().text(db), + display_type_ref(db, field_def.ty()) + ), + documentation: comments_markdown( + contract.field_leading_comments(db, field.index.as_usize())?, + ), + }) +} + +fn class_method_hover<'db>( + db: &'db vfs::AnalysisHost, + class: DefId<'db>, + name: &str, +) -> Option { + let module = parser::parse_file_to_hir(db, class.file(db)).module(db); + let Definition::Class(class_def) = find_definition(db, module, class)? else { + return None; + }; + let (index, sig) = class_def + .methods(db) + .iter() + .enumerate() + .find(|(_, method)| method.name.atom().text(db) == name)?; + let module_id = nameres::module_id_for_source_file(db, class.file(db))?; + let scheme = hir_ty::infer::class_method_scheme(db, module_id, class, name.to_owned())?; + let type_var_names = ident_names(db, class_def.type_var_elems(db)); + Some(HoverInfo { + code: format!( + "function {}", + format_callable_scheme( + db, + name, + &function_param_names(db, sig), + &type_var_names, + scheme, + Some(sig), + None, + ) + ), + documentation: comments_markdown(class_def.method_leading_comments(db, index)?), + }) +} + +struct FunctionOwner<'db> { + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, +} + +struct BodyAnalysis<'db> { + inference: InferenceResult<'db>, + resolutions: hir::nameres::BodyResolutionMap<'db>, + type_var_names: Vec, +} + +fn parameter_hover<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + imports: &dyn hir::nameres::ImportedNames<'db>, + param: ParamId<'db>, +) -> Option { + let owner = function_owner_for_body(db, module, param.body)?; + let parameter = parameter_for_body(db, &owner, param.body, param.index.as_usize())?; + let analysis = infer_function(db, module, current_module, imports, &owner); + let inferred = if param.body == owner.root_body { + root_parameter_ty(db, &analysis.inference, param.index.as_usize()) + } else { + parameter_reference_ty(&analysis, param) + }; + let (name, annotated, comptime) = match ¶meter { + FuncParam::Typed { comptime, name, ty } => ( + name.atom().text(db).to_owned(), + Some(display_type_ref(db, *ty)), + comptime.is_some(), + ), + FuncParam::Untyped { comptime, name } => { + (name.atom().text(db).to_owned(), None, comptime.is_some()) + } + FuncParam::Error { .. } => return None, + }; + let ty = annotated + .or_else(|| inferred.map(|ty| display_ty(db, ty, &analysis.type_var_names))) + .unwrap_or_else(|| "_".to_owned()); + let prefix = if comptime { "comptime " } else { "" }; + Some(HoverInfo { + code: format!("{prefix}{name}: {ty}"), + documentation: None, + }) +} + +fn local_hover<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + imports: &dyn hir::nameres::ImportedNames<'db>, + local: &LocalBinding<'db>, +) -> Option { + let body = match local { + LocalBinding::Let { body, .. } | LocalBinding::Pattern { body, .. } => *body, + LocalBinding::TypeVar(type_var) => { + return Some(HoverInfo { + code: format!("type parameter {}", type_var.name), + documentation: None, + }); + } + }; + let owner = function_owner_for_body(db, module, body)?; + let analysis = infer_function(db, module, current_module, imports, &owner); + + match local { + LocalBinding::Let { body, stmt } => { + let statement = body.stmts(db).get(*stmt); + let StmtKind::Let { + comptime, + name, + ty: annotation, + .. + } = &statement.kind + else { + return None; + }; + let ty = annotation + .map(|ty| display_type_ref(db, ty)) + .or_else(|| { + analysis + .inference + .let_ty(*body, *stmt) + .map(|ty| display_ty(db, ty, &analysis.type_var_names)) + }) + .unwrap_or_else(|| "_".to_owned()); + let prefix = if comptime.is_some() { "comptime " } else { "" }; + Some(HoverInfo { + code: format!("{prefix}let {}: {ty}", name.atom().text(db)), + documentation: None, + }) + } + LocalBinding::Pattern { body, pat } => { + let pattern = body.pats(db).get(*pat); + let PatKind::Var(name) = &pattern.kind else { + return None; + }; + let ty = source_type_for_pattern_binding(db, *body, *pat) + .map(|ty| display_type_ref(db, ty)) + .or_else(|| { + analysis + .inference + .pat_ty(*body, *pat) + .map(|ty| display_ty(db, ty, &analysis.type_var_names)) + }) + .unwrap_or_else(|| "_".to_owned()); + Some(HoverInfo { + code: format!("{}: {ty}", name.atom().text(db)), + documentation: None, + }) + } + LocalBinding::TypeVar(_) => unreachable!("handled above"), + } +} + +fn source_type_for_pattern_binding<'db>( + db: &'db dyn hir_ty::Db, + body: FuncBody<'db>, + target: hir::arena::Id>, +) -> Option> { + for (_, statement) in body.stmts(db).iter() { + let StmtKind::Match { scrutinees, arms } = &statement.kind else { + continue; + }; + for arm in arms { + for (root, scrutinee) in arm.pats.iter().zip(scrutinees) { + let Some(path) = tuple_pattern_path(db, body, *root, target) else { + continue; + }; + let ExprKind::TypeAscription { ty, .. } = &body.exprs(db).get(*scrutinee).kind + else { + continue; + }; + return tuple_type_at_path(db, *ty, &path); + } + } + } + None +} + +fn tuple_pattern_path<'db>( + db: &'db dyn hir_ty::Db, + body: FuncBody<'db>, + current: hir::arena::Id>, + target: hir::arena::Id>, +) -> Option> { + if current == target { + return Some(Vec::new()); + } + let PatKind::Tuple { elems } = &body.pats(db).get(current).kind else { + return None; + }; + for (index, child) in elems.iter().enumerate() { + if let Some(mut path) = tuple_pattern_path(db, body, *child, target) { + path.insert(0, index); + return Some(path); + } + } + None +} + +fn tuple_type_at_path<'db>( + db: &'db dyn hir_ty::Db, + mut ty: TypeRef<'db>, + path: &[usize], +) -> Option> { + for index in path { + let TypeRefKind::Tuple { elems } = ty.kind(db) else { + return None; + }; + ty = *elems.atom().get(*index)?; + } + Some(ty) +} + +fn root_parameter_ty<'db>( + db: &'db dyn hir_ty::Db, + inference: &InferenceResult<'db>, + index: usize, +) -> Option> { + let TyKind::Function { params, .. } = inference.root_scheme.body(db).ty(db).kind(db) else { + return None; + }; + params.get(index).copied() +} + +fn parameter_reference_ty<'db>( + analysis: &BodyAnalysis<'db>, + param: ParamId<'db>, +) -> Option> { + analysis.resolutions.exprs.iter().find_map(|entry| { + if matches!(&entry.resolution, Resolution::Param(candidate) if *candidate == param) { + analysis.inference.expr_ty(entry.body, entry.expr) + } else { + None + } + }) +} + +fn parameter_for_body<'db>( + db: &'db dyn hir_ty::Db, + owner: &FunctionOwner<'db>, + target: FuncBody<'db>, + index: usize, +) -> Option> { + if owner.root_body == target { + return owner.function.sig(db).params.atom().get(index).cloned(); + } + + let mut stack = vec![owner.root_body]; + while let Some(body) = stack.pop() { + for (_, expr) in body.exprs(db).iter() { + if let ExprKind::Lambda { + params, + body: lambda_body, + .. + } = &expr.kind + { + if *lambda_body == target { + return params.atom().get(index).cloned(); + } + stack.push(*lambda_body); + } + } + } + None +} + +fn function_owner_for_body<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + target: FuncBody<'db>, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + if let Some(owner) = make_owner_if_contains(db, function, None, Vec::new(), target) + { + return Some(owner); + } + } + Item::ContractDef(contract) => { + let inherited_type_vars = hir::nameres::type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + ); + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item + && let Some(owner) = make_owner_if_contains( + db, + function, + Some(contract.def_id_value(db)), + inherited_type_vars.clone(), + target, + ) + { + return Some(owner); + } + } + } + Item::InstanceDef(instance) => { + let inherited_type_vars = hir::nameres::type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + ); + for function in instance.methods(db) { + if let Some(owner) = make_owner_if_contains( + db, + *function, + None, + inherited_type_vars.clone(), + target, + ) { + return Some(owner); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + None +} + +fn make_owner_if_contains<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, + target: FuncBody<'db>, +) -> Option> { + let root_body = function.body(db)?; + body_tree_contains(db, root_body, target).then_some(FunctionOwner { + function, + root_body, + enclosing_contract, + inherited_type_vars, + }) +} + +fn body_tree_contains<'db>( + db: &'db dyn hir_ty::Db, + root: FuncBody<'db>, + target: FuncBody<'db>, +) -> bool { + let mut stack = vec![root]; + while let Some(body) = stack.pop() { + if body == target { + return true; + } + for (_, expr) in body.exprs(db).iter() { + if let ExprKind::Lambda { + body: lambda_body, .. + } = &expr.kind + { + stack.push(*lambda_body); + } + } + } + false +} + +fn infer_function<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + current_module: nameres::ModuleId<'db>, + imports: &dyn hir::nameres::ImportedNames<'db>, + owner: &FunctionOwner<'db>, +) -> BodyAnalysis<'db> { + let scope = hir::nameres::item_scope_facts(db, module); + let item_facts = + hir::nameres::resolve_item_type_facts_with_imports(db, module, &scope, imports); + let sig = owner.function.sig(db); + let mut type_vars = owner.inherited_type_vars.clone(); + type_vars.extend(hir::nameres::type_var_bindings( + owner.function.def_id_value(db), + &sig.type_vars, + )); + let type_var_names = type_vars + .iter() + .map(|var| var.name.atom().text(db).to_owned()) + .collect::>(); + let body_context = hir::nameres::BodyResolutionContext { + module, + enclosing_contract: owner.enclosing_contract, + params: hir::nameres::param_bindings(sig.params.atom()), + type_vars: type_vars.clone(), + }; + let resolutions = hir::nameres::resolve_body_with_imports_and_policy( + db, + owner.root_body, + &body_context, + imports, + hir::nameres::NameresDiagnosticPolicy::Emit, + ); + let lowered = hir_ty::lower_normalized_function_with_inferred_signature( + db, + module, + &item_facts, + owner.function, + &type_vars, + Some(&resolutions), + Some(current_module), + ); + let param_names = function_param_names(db, sig); + let ty_context = hir_ty::BodyTyContext::new( + module, + resolutions.clone(), + type_vars, + lowered.params.clone(), + Some(lowered.ret), + ) + .with_param_names(param_names) + .with_entry_module(current_module) + .with_pre_typeck_desugar(hir_ty::pre_typeck_desugar_body_tree(db, owner.root_body)); + let inference = hir_ty::infer_body(db, owner.root_body, ty_context); + + BodyAnalysis { + inference, + resolutions, + type_var_names, + } +} + +fn format_callable_scheme<'db>( + db: &'db dyn hir_ty::Db, + name: &str, + param_names: &[String], + type_var_names: &[String], + scheme: TyScheme<'db>, + source_sig: Option<&FuncSig<'db>>, + source_params: Option<&[TypeRef<'db>]>, +) -> String { + let ty = scheme.body(db).ty(db); + let (params, ret) = match ty.kind(db) { + TyKind::Function { params, ret } => (params.as_slice(), *ret), + _ => (&[][..], ty), + }; + let params = params + .iter() + .enumerate() + .map(|(index, param)| { + let source_param = source_sig.and_then(|sig| sig.params.atom().get(index)); + let ty = source_param + .and_then(|param| match param { + FuncParam::Typed { ty, .. } => Some(display_type_ref(db, *ty)), + FuncParam::Untyped { .. } | FuncParam::Error { .. } => None, + }) + .or_else(|| { + source_params + .and_then(|params| params.get(index)) + .map(|ty| display_type_ref(db, *ty)) + }) + .unwrap_or_else(|| display_ty(db, *param, type_var_names)); + if let Some( + FuncParam::Typed { comptime, name, .. } | FuncParam::Untyped { comptime, name }, + ) = source_param + { + let prefix = if comptime.is_some() { "comptime " } else { "" }; + return format!("{prefix}{}: {ty}", name.atom().text(db)); + } + param_names + .get(index) + .map(|name| format!("{name}: {ty}")) + .unwrap_or(ty) + }) + .collect::>() + .join(", "); + let mut signature = format!("{name}({params})"); + if let Some(sig) = source_sig { + if let Some(visibility) = sig.visibility_kind() { + signature.push(' '); + signature.push_str(visibility.keyword()); + } + if let Some(mutability) = sig.mutability_kind() { + signature.push(' '); + signature.push_str(mutability.keyword()); + } + } + if let Some(source_ret) = source_sig.and_then(|sig| sig.ret) { + signature.push_str(&display_type_ref_return_suffix(db, source_ret)); + } else { + signature.push_str(&display_ty_return_suffix(db, ret, type_var_names)); + } + let predicates = scheme + .body(db) + .preds(db) + .iter() + .map(|pred| display_pred(db, *pred, type_var_names)) + .collect::>(); + if !predicates.is_empty() { + signature.push_str(" where "); + signature.push_str(&predicates.join(", ")); + } + signature +} + +fn display_ty<'db>(db: &'db dyn hir_ty::Db, ty: Ty<'db>, names: &[String]) -> String { + match ty.kind(db) { + TyKind::Error => "".to_owned(), + TyKind::Unknown => "_".to_owned(), + TyKind::BoundVar(var) => names + .get(var.index as usize) + .cloned() + .unwrap_or_else(|| "_".to_owned()), + TyKind::Named { ctor, args } => { + if let TyCtor::Builtin(hir_ty::BuiltinTyCtor::FixedArray(length)) = ctor + && let [element] = args.as_slice() + { + return format!("{}[{length}]", display_ty(db, *element, names)); + } + let name = match ctor { + TyCtor::Builtin(ctor) => ctor.name().to_owned(), + TyCtor::User(user) => user + .def + .name(db) + .unwrap_or_else(|| format!("{:?}", user.def.kind(db))), + }; + if args.is_empty() { + name + } else if name == "DynArray" && args.len() == 1 { + format!("{}[]", display_ty(db, args[0], names)) + } else if matches!(name.as_str(), "memory" | "storage" | "calldata") && args.len() == 1 + { + format!("{} {name}", display_ty(db, args[0], names)) + } else if name == "mapping" && args.len() == 2 { + format!( + "mapping({} => {})", + display_ty(db, args[0], names), + display_ty(db, args[1], names) + ) + } else { + format!( + "{name}<{}>", + args.iter() + .map(|arg| display_ty(db, *arg, names)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Function { params, ret } => format!( + "function({}){}", + params + .iter() + .map(|param| display_ty(db, *param, names)) + .collect::>() + .join(", "), + display_ty_return_suffix(db, *ret, names) + ), + TyKind::Tuple(elems) => { + if elems.is_empty() { + "()".to_owned() + } else { + format!( + "({})", + elems + .iter() + .map(|elem| display_ty(db, *elem, names)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Comptime(inner) => format!("comptime {}", display_ty(db, *inner, names)), + } +} + +fn display_ty_return_suffix<'db>( + db: &'db dyn hir_ty::Db, + ret: Ty<'db>, + names: &[String], +) -> String { + match ret.kind(db) { + TyKind::Tuple(elems) if elems.is_empty() => String::new(), + TyKind::Tuple(elems) => format!( + " returns ({})", + elems + .iter() + .map(|elem| display_ty(db, *elem, names)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", display_ty(db, ret, names)), + } +} + +fn display_pred<'db>(db: &'db dyn hir_ty::Db, pred: hir_ty::Pred<'db>, names: &[String]) -> String { + match pred.kind(db) { + PredKind::InClass { class, main, args } => { + let class = match class { + ClassId::Builtin(class) => class.name().to_owned(), + ClassId::User(def) => def + .name(db) + .unwrap_or_else(|| format!("{:?}", def.kind(db))), + }; + if args.is_empty() { + format!("{}: {class}", display_ty(db, *main, names)) + } else { + format!( + "{}: {class}<{}>", + display_ty(db, *main, names), + args.iter() + .map(|arg| display_ty(db, *arg, names)) + .collect::>() + .join(", ") + ) + } + } + PredKind::Eq { lhs, rhs } => format!( + "{} ~ {}", + display_ty(db, *lhs, names), + display_ty(db, *rhs, names) + ), + PredKind::Error => "".to_owned(), + } +} + +fn display_type_ref<'db>(db: &'db dyn hir_ty::Db, ty: TypeRef<'db>) -> String { + hir_ty::display_type_ref_source(db, ty) +} + +fn display_type_ref_return_suffix<'db>(db: &'db dyn hir_ty::Db, ret: TypeRef<'db>) -> String { + match ret.kind(db) { + TypeRefKind::Tuple { elems } if elems.atom().is_empty() => String::new(), + TypeRefKind::Tuple { elems } => format!( + " returns ({})", + elems + .atom() + .iter() + .map(|elem| display_type_ref(db, *elem)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", display_type_ref(db, ret)), + } +} + +fn display_pred_ref<'db>(db: &'db dyn hir_ty::Db, pred: PredRef<'db>) -> String { + let kind = pred.kind(db); + let ty = display_type_ref(db, kind.ty); + let class = kind.class.atom().text(db); + if kind.args.atom().is_empty() { + format!("{ty}: {class}") + } else { + format!( + "{ty}: {class}<{}>", + kind.args + .atom() + .iter() + .map(|arg| display_type_ref(db, *arg)) + .collect::>() + .join(", ") + ) + } +} + +fn display_trait_ref<'db>(db: &'db dyn hir_ty::Db, pred: PredRef<'db>) -> String { + let kind = pred.kind(db); + let mut args = Vec::with_capacity(kind.args.atom().len() + 1); + args.push(display_type_ref(db, kind.ty)); + args.extend( + kind.args + .atom() + .iter() + .map(|arg| display_type_ref(db, *arg)), + ); + format!("{}<{}>", kind.class.atom().text(db), args.join(", ")) +} + +fn append_where_clause<'db>(db: &'db dyn hir_ty::Db, out: &mut String, preds: &[PredRef<'db>]) { + if preds.is_empty() { + return; + } + out.push_str(" where "); + out.push_str( + &preds + .iter() + .map(|pred| display_pred_ref(db, *pred)) + .collect::>() + .join(", "), + ); +} + +#[cfg(test)] +mod tests { + use lsp_types::{HoverContents, MarkedString}; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn hover_at(source: &str, world: &WorldState, uri: &Url, offset: usize) -> Hover { + let position = world + .line_index(uri) + .expect("line index") + .byte_to_position(offset as u32); + handle_hover(world, uri, position).unwrap_or_else(|| { + panic!( + "hover at byte {offset} (`{}`)", + &source[offset..source.len().min(offset + 12)] + ) + }) + } + + fn hover_code(hover: &Hover) -> &str { + let marked = match &hover.contents { + HoverContents::Scalar(marked) => marked, + HoverContents::Array(marked) => marked.first().expect("hover code"), + HoverContents::Markup(markup) => return &markup.value, + }; + match marked { + MarkedString::LanguageString(value) => &value.value, + MarkedString::String(value) => value, + } + } + + #[test] + fn identifier_range_supports_unicode_and_internal_hyphens() { + let text = "prefix λ-value suffix"; + let start = text.find('λ').expect("unicode identifier") as u32; + let end = start + "λ-value".len() as u32; + + assert_eq!(identifier_range(text, start), Some((start, end))); + assert_eq!( + identifier_range(text, start + "λ-".len() as u32), + Some((start, end)) + ); + assert_eq!(identifier_range(text, start + 1), None); + } + + #[test] + fn hovers_integer_literal_type() { + let source = "function main() returns (word) {\n return 42;\n}\n"; + let (world, uri) = world_with_main(source); + let literal_offset = source.find("42").expect("literal"); + + let hover = hover_at(source, &world, &uri, literal_offset); + let display = hover_code(&hover); + + assert!( + display.contains("word"), + "expected word type in hover display, got {display}" + ); + assert_eq!( + hover.range, + Some( + world + .line_index(&uri) + .expect("line index") + .range(literal_offset as u32, literal_offset as u32 + 2) + ) + ); + } + + #[test] + fn function_and_parameter_references_show_signatures_and_identifier_ranges() { + let source = "\ +// Returns its input. +function id(x: word) returns (word) { + return x; +} + +function main() returns (word) { + return id(42); +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + + let call = source.rfind("id(42)").expect("call"); + let function_hover = hover_at(source, &world, &uri, call); + assert!( + hover_code(&function_hover).contains("function id(x: word) returns (word)"), + "unexpected function hover: {:?}", + function_hover.contents + ); + assert_eq!( + function_hover.range, + Some(line_index.range(call as u32, call as u32 + 2)) + ); + assert!( + matches!(function_hover.contents, HoverContents::Array(_)), + "leading documentation should be included" + ); + + let parameter = source.find("return x").expect("parameter use") + "return ".len(); + let parameter_hover = hover_at(source, &world, &uri, parameter); + assert_eq!(hover_code(¶meter_hover), "x: word"); + assert_eq!( + parameter_hover.range, + Some(line_index.range(parameter as u32, parameter as u32 + 1)) + ); + } + + #[test] + fn special_function_hovers_show_the_keyword_once() { + let source = "\ +contract Wallet { + constructor(owner: address) payable {} + fallback() external payable {} +} +"; + let (world, uri) = world_with_main(source); + + let constructor = source.find("constructor").expect("constructor"); + let constructor_hover = hover_at(source, &world, &uri, constructor); + assert_eq!( + hover_code(&constructor_hover), + "constructor(owner: address) payable" + ); + + let fallback = source.find("fallback").expect("fallback"); + let fallback_hover = hover_at(source, &world, &uri, fallback); + assert_eq!(hover_code(&fallback_hover), "fallback() external payable"); + } + + #[test] + fn inferred_local_reference_hover_uses_local_name_range() { + let source = "\ +function main() returns (word) { + let result = 42; + return result; +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let reference = source.rfind("result").expect("local reference"); + + let hover = hover_at(source, &world, &uri, reference); + + assert!( + hover_code(&hover).contains("let result: word"), + "unexpected local hover: {:?}", + hover.contents + ); + assert_eq!( + hover.range, + Some(line_index.range(reference as u32, reference as u32 + "result".len() as u32)) + ); + } + + #[test] + fn type_and_constructor_references_have_rich_hover_and_leaf_ranges() { + let source = "\ +enum Maybe { None, Some(word) } + +function main() returns (Maybe) { + return Maybe.Some(42); +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + + let ty_reference = source.rfind("Maybe").expect("type reference"); + let ty_hover = hover_at(source, &world, &uri, ty_reference); + assert!( + hover_code(&ty_hover).contains("enum Maybe { None, Some(word) }"), + "unexpected type hover: {:?}", + ty_hover.contents + ); + assert_eq!( + ty_hover.range, + Some(line_index.range(ty_reference as u32, ty_reference as u32 + 5)) + ); + + let ctor_reference = source.rfind("Some(42)").expect("constructor reference"); + let ctor_hover = hover_at(source, &world, &uri, ctor_reference); + let ctor_code = hover_code(&ctor_hover); + assert!( + ctor_code.contains("Some(word) returns (Maybe)"), + "unexpected constructor hover: {ctor_code}" + ); + assert_eq!( + ctor_hover.range, + Some(line_index.range(ctor_reference as u32, ctor_reference as u32 + 4)) + ); + } + + #[test] + fn type_declaration_hover_preserves_alias_and_value_type_spelling() { + let source = "\ +alias WordAlias = word; +alias Words = word[4]; +type Wad is word; + +function expose(alias_value: WordAlias, words: Words, wad_value: Wad) {} +"; + let (world, uri) = world_with_main(source); + + let alias_reference = source.rfind("WordAlias").expect("alias reference"); + let alias_hover = hover_at(source, &world, &uri, alias_reference); + assert_eq!(hover_code(&alias_hover), "alias WordAlias = word"); + + let words_reference = source.rfind("Words").expect("fixed-array alias reference"); + let words_hover = hover_at(source, &world, &uri, words_reference); + assert_eq!(hover_code(&words_hover), "alias Words = word[4]"); + + let value_type_reference = source.rfind("Wad").expect("value type reference"); + let value_type_hover = hover_at(source, &world, &uri, value_type_reference); + assert_eq!(hover_code(&value_type_hover), "type Wad is word"); + } + + #[test] + fn contract_like_declaration_hover_preserves_shell_kind() { + let source = "\ +interface Reader { + function read(key: word) external view returns (word); +} + +library Helpers { + function identity(value: word) internal pure returns (word) { return value; } +} +"; + let (world, uri) = world_with_main(source); + + let interface = source.find("Reader").expect("interface declaration"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, interface)), + "interface Reader" + ); + + let library = source.find("Helpers").expect("library declaration"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, library)), + "library Helpers" + ); + + let read = source.find("read(").expect("interface method"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, read)), + "function read(key: word) external view returns (word)" + ); + + let identity = source.find("identity(").expect("library method"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, identity)), + "function identity(value: word) internal pure returns (word)" + ); + } + + #[test] + fn function_type_hovers_preserve_source_qualifiers() { + let source = "\ +alias Callback = function(word) internal pure returns (bool); + +contract Registry { + handler: function(word) external view returns (bool); +} + +function inspect( + comptime callback: function(word) external view returns (bool) +) returns (function(address) internal payable returns (word)) { + let local: function(address) internal payable returns (word) = callback; + return local; +} +"; + let (world, uri) = world_with_main(source); + + let alias = source.find("Callback").expect("alias declaration"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, alias)), + "alias Callback = function(word) internal pure returns (bool)" + ); + + let field = source.find("handler").expect("field declaration"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, field)), + "handler: function(word) external view returns (bool)" + ); + + let function = source.find("inspect").expect("function declaration"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, function)), + "function inspect(comptime callback: function(word) external view returns (bool)) returns (function(address) internal payable returns (word))" + ); + + let parameter = source.rfind("callback").expect("parameter reference"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, parameter)), + "comptime callback: function(word) external view returns (bool)" + ); + + let local = source.rfind("local").expect("local reference"); + assert_eq!( + hover_code(&hover_at(source, &world, &uri, local)), + "let local: function(address) internal payable returns (word)" + ); + } + + #[test] + fn destructured_local_hover_preserves_function_type_qualifiers() { + let source = "function inspect(value: (function(word) external view returns (bool), word)) {\n let (destructured, seed): (function(word) external view returns (bool), word) = value;\n destructured;\n}\n"; + let (world, uri) = world_with_main(source); + let local = source.rfind("destructured").expect("local reference"); + + assert_eq!( + hover_code(&hover_at(source, &world, &uri, local)), + "destructured: function(word) external view returns (bool)" + ); + } + + #[test] + fn constructor_hover_preserves_function_type_qualifiers() { + let source = + "enum CallbackBox { CallbackBox(function(word) external view returns (bool)) }\n"; + let (world, uri) = world_with_main(source); + let constructor = source.rfind("CallbackBox(").expect("constructor"); + + assert_eq!( + hover_code(&hover_at(source, &world, &uri, constructor)), + "constructor CallbackBox(function(word) external view returns (bool)) returns (CallbackBox)" + ); + } +} diff --git a/crates/lsp/src/import_edits.rs b/crates/lsp/src/import_edits.rs new file mode 100644 index 00000000..00bd777b --- /dev/null +++ b/crates/lsp/src/import_edits.rs @@ -0,0 +1,863 @@ +//! Source edit planning for missing-import quick fixes. +//! +//! This module deliberately stops at byte edits. The code-action layer owns +//! URI mapping and conversion to LSP UTF-16 ranges, while this planner owns the +//! syntax-sensitive choice between extending a selective import and inserting +//! a new declaration. + +use hir::{ + ast::item::{Import, ImportSelector, Item, Module}, + span::Spanned, +}; + +/// One replacement over UTF-8 source byte offsets. +/// +/// Import edits are currently insertions, so `start == end`. Keeping both +/// bounds makes the result directly adaptable to compiler and LSP text edits. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ImportEdit { + /// Inclusive byte offset at which replacement starts. + pub start: u32, + /// Exclusive byte offset at which replacement ends. + pub end: u32, + /// Source text to insert or replace. + pub replacement: String, +} + +/// Plans one deterministic edit that brings `public_name` into scope. +/// +/// When the target already has a safe explicit `{...} from` import, the name is +/// appended to that selector. Otherwise a separate selective import is placed +/// after the existing import block, or after leading pragmas/header comments. +/// Malformed source, stale parse metadata, and text that cannot be represented +/// safely as import syntax produce no edit. +pub fn plan_import_edit<'db>( + db: &'db dyn parser::Db, + source: &str, + parsed: parser::ParseHirOutput<'db>, + target_import_path: &str, + public_name: &str, +) -> Option { + let source_len = u32::try_from(source.len()).ok()?; + if !parser::is_valid_identifier(public_name) + || !is_valid_import_path(target_import_path) + || !parsed.diagnostics(db).is_empty() + { + return None; + } + + let module = parsed.module(db); + if !metadata_matches_source(db, module, source, source_len) { + return None; + } + + let imports = module + .items(db) + .iter() + .filter_map(|item| match item { + Item::Import(import) => Some(*import), + _ => None, + }) + .collect::>(); + + let mut append_target = None; + for import in imports + .iter() + .copied() + .filter(|import| import_path_text(db, *import).as_deref() == Some(target_import_path)) + { + let Some(ImportSelector::Names(names)) = import.selector(db) else { + continue; + }; + + let source_is_hidden = |selected: &hir::ast::item::SelectedName<'db>| { + import + .hiding(db) + .iter() + .any(|hidden| hidden.name.atom().text(db) == selected.name.atom().text(db)) + }; + + // Adding another selector with the same active local name would be + // ambiguous. Hidden selections bind nothing, so they neither suppress + // the quick fix nor make the new selector ambiguous. + if names + .iter() + .filter(|selected| !source_is_hidden(selected)) + .any(|selected| { + selected + .alias + .as_ref() + .is_some_and(|alias| alias.atom().text(db) == public_name) + }) + { + return None; + } + if names + .iter() + .filter(|selected| !source_is_hidden(selected)) + .any(|selected| { + selected.name.atom().text(db) == public_name && selected.alias.is_none() + }) + { + return None; + } + + // Keep an import containing a hidden spelling that would otherwise + // collide untouched. A clean declaration is easier to reason about + // than changing the meaning of an existing `hiding` clause. + if names.iter().any(|selected| { + source_is_hidden(selected) + && (selected.name.atom().text(db) == public_name + || selected + .alias + .as_ref() + .is_some_and(|alias| alias.atom().text(db) == public_name)) + }) { + continue; + } + if names + .iter() + .filter(|selected| !source_is_hidden(selected)) + .any(|selected| selected.name.atom().text(db) == public_name) + || import + .hiding(db) + .iter() + .any(|hidden| hidden.name.atom().text(db) == public_name) + { + continue; + } + + if append_target.is_none() { + append_target = selector_append_offset(db, source, import, names); + } + } + + if let Some(offset) = append_target { + return Some(insertion(offset, format!(", {public_name}"))); + } + + plan_new_import( + db, + source, + module, + &imports, + target_import_path, + public_name, + ) +} + +/// Plans a deterministic namespace import such as +/// `import * as math from lib.math;`. +/// +/// The local namespace name is the target path's leaf. Namespace imports are +/// never merged with bare or selective imports. An identical namespace import +/// already present in the source needs no edit. +/// Validation, stale-source rejection, and insertion placement are shared with +/// [`plan_import_edit`]. +pub fn plan_module_import_edit<'db>( + db: &'db dyn parser::Db, + source: &str, + parsed: parser::ParseHirOutput<'db>, + target_import_path: &str, +) -> Option { + let source_len = u32::try_from(source.len()).ok()?; + let leaf = target_import_path.rsplit('.').next()?; + let alias = leaf.strip_prefix('@').unwrap_or(leaf); + if !is_valid_import_path(target_import_path) + || !parser::is_valid_identifier(alias) + || !parsed.diagnostics(db).is_empty() + { + return None; + } + + let module = parsed.module(db); + if !metadata_matches_source(db, module, source, source_len) { + return None; + } + + let imports = module + .items(db) + .iter() + .filter_map(|item| match item { + Item::Import(import) => Some(*import), + _ => None, + }) + .collect::>(); + + if imports.iter().copied().any(|import| { + import_path_text(db, import).as_deref() == Some(target_import_path) + && import.selector(db).is_none() + && import + .alias_elem(db) + .is_some_and(|existing| existing.atom().text(db) == alias) + && import.hiding(db).is_empty() + }) { + return None; + } + + plan_new_import_declaration( + db, + source, + module, + &imports, + &format!("import * as {alias} from {target_import_path};"), + ) +} + +fn metadata_matches_source<'db>( + db: &'db dyn parser::Db, + module: Module<'db>, + source: &str, + source_len: u32, +) -> bool { + let absolute = module.span(db).resolve_to_absolute(db); + absolute.start().as_u32() == 0 + && absolute.end().as_u32() == source_len + && absolute.file().content(db).as_deref() == Some(source) +} + +fn is_valid_import_path(path: &str) -> bool { + let path = path.strip_prefix('@').unwrap_or(path); + !path.is_empty() && path.split('.').all(parser::is_valid_identifier) +} + +fn import_path_text(db: &dyn parser::Db, import: Import<'_>) -> Option { + let mut path = String::new(); + if import.external(db).is_some() { + path.push('@'); + } + for (index, element) in import.path_elems(db).iter().enumerate() { + if index > 0 { + path.push('.'); + } + path.push_str(element.atom().text(db)); + } + (!import.path_elems(db).is_empty()).then_some(path) +} + +fn selector_append_offset<'db>( + db: &'db dyn parser::Db, + source: &str, + import: Import<'db>, + names: &[hir::ast::item::SelectedName<'db>], +) -> Option { + let last = names.last()?; + let last_span = last + .alias + .as_ref() + .map_or_else(|| last.name.span(db), |alias| alias.span(db)); + let last_absolute = last_span.resolve_to_absolute(db); + let import_absolute = import.span(db).resolve_to_absolute(db); + if last_absolute.file() != import_absolute.file() { + return None; + } + + let insertion_offset = usize::try_from(last_absolute.end().as_u32()).ok()?; + let import_start = usize::try_from(import_absolute.start().as_u32()).ok()?; + let import_end = usize::try_from(import_absolute.end().as_u32()).ok()?; + if import_start > insertion_offset + || insertion_offset > import_end + || import_end > source.len() + || !source.is_char_boundary(insertion_offset) + { + return None; + } + + let selector_close = matching_selector_close(source, import_start, import_end)?; + if insertion_offset > selector_close + || !contains_only_trivia(&source[insertion_offset..selector_close]) + { + return None; + } + + u32::try_from(insertion_offset).ok() +} + +fn matching_selector_close(source: &str, start: usize, end: usize) -> Option { + let bytes = source.get(start..end)?.as_bytes(); + let mut index = 0usize; + let mut depth = 0usize; + let mut selector_open = None; + + while index < bytes.len() { + if bytes[index..].starts_with(b"//") { + index = skip_line_comment(bytes, index); + continue; + } + if bytes[index..].starts_with(b"/*") { + index = skip_block_comment(bytes, index)?; + continue; + } + match bytes[index] { + b'{' => { + if selector_open.is_none() { + selector_open = Some(index); + } + depth += 1; + } + b'}' if selector_open.is_some() => { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(start + index); + } + } + _ => {} + } + index += 1; + } + None +} + +fn contains_only_trivia(text: &str) -> bool { + let bytes = text.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + if bytes[index].is_ascii_whitespace() { + index += 1; + } else if bytes[index..].starts_with(b"//") { + index = skip_line_comment(bytes, index); + } else if bytes[index..].starts_with(b"/*") { + let Some(next) = skip_block_comment(bytes, index) else { + return false; + }; + index = next; + } else { + return false; + } + } + true +} + +fn plan_new_import( + db: &dyn parser::Db, + source: &str, + module: Module<'_>, + imports: &[Import<'_>], + target_import_path: &str, + public_name: &str, +) -> Option { + let declaration = format!("import {{{public_name}}} from {target_import_path};"); + plan_new_import_declaration(db, source, module, imports, &declaration) +} + +fn plan_new_import_declaration( + db: &dyn parser::Db, + source: &str, + module: Module<'_>, + imports: &[Import<'_>], + declaration: &str, +) -> Option { + let line_ending = preferred_line_ending(source); + + if let Some(last_import) = imports.last() { + let end = absolute_span_end(db, last_import.span(db), source.len())?; + return insert_after_declaration_line(source, end, declaration, line_ending); + } + + let mut last_leading_pragma = None; + for item in module.items(db) { + match item { + Item::Pragma(pragma) => { + last_leading_pragma = Some(*pragma); + } + _ => break, + } + } + if let Some(pragma) = last_leading_pragma { + let end = absolute_span_end(db, pragma.span(db), source.len())?; + return insert_after_declaration_line(source, end, declaration, line_ending); + } + + if let Some(comment_end) = leading_header_comment_end(source) { + return insert_after_declaration_line(source, comment_end, declaration, line_ending); + } + + Some(insertion(0, format!("{declaration}{line_ending}"))) +} + +fn absolute_span_end( + db: &dyn parser::Db, + span: hir::span::Span<'_>, + source_len: usize, +) -> Option { + let end = usize::try_from(span.resolve_to_absolute(db).end().as_u32()).ok()?; + (end <= source_len).then_some(end) +} + +fn insert_after_declaration_line( + source: &str, + declaration_end: usize, + declaration: &str, + line_ending: &str, +) -> Option { + if declaration_end > source.len() || !source.is_char_boundary(declaration_end) { + return None; + } + + match safe_end_of_line(source, declaration_end)? { + LineInsertion::AtNextLine(offset) => Some(insertion( + u32::try_from(offset).ok()?, + format!("{declaration}{line_ending}"), + )), + LineInsertion::AtEndOfFile(offset) => Some(insertion( + u32::try_from(offset).ok()?, + format!("{line_ending}{declaration}"), + )), + LineInsertion::BeforeSameLineCode(offset) => Some(insertion( + u32::try_from(offset).ok()?, + format!("{line_ending}{declaration}{line_ending}"), + )), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum LineInsertion { + AtNextLine(usize), + AtEndOfFile(usize), + BeforeSameLineCode(usize), +} + +/// Finds a point after same-line trailing comments, never inside a block +/// comment. Once the terminating line break is consumed, comments on the next +/// line remain in place (they may document the following declaration). +fn safe_end_of_line(source: &str, mut index: usize) -> Option { + let bytes = source.as_bytes(); + loop { + while index < bytes.len() && matches!(bytes[index], b' ' | b'\t' | 0x0c) { + index += 1; + } + if index == bytes.len() { + return Some(LineInsertion::AtEndOfFile(index)); + } + if bytes[index..].starts_with(b"//") { + index = skip_line_comment(bytes, index); + } else if bytes[index..].starts_with(b"/*") { + index = skip_block_comment(bytes, index)?; + continue; + } + + if index == bytes.len() { + return Some(LineInsertion::AtEndOfFile(index)); + } + if bytes[index] == b'\r' { + let end = if bytes.get(index + 1) == Some(&b'\n') { + index + 2 + } else { + index + 1 + }; + return Some(LineInsertion::AtNextLine(end)); + } + if bytes[index] == b'\n' { + return Some(LineInsertion::AtNextLine(index + 1)); + } + return Some(LineInsertion::BeforeSameLineCode(index)); + } +} + +fn leading_header_comment_end(source: &str) -> Option { + let bytes = source.as_bytes(); + let mut index = 0usize; + let mut last_comment_end = None; + loop { + while index < bytes.len() && bytes[index].is_ascii_whitespace() { + index += 1; + } + if bytes + .get(index..) + .is_some_and(|tail| tail.starts_with(b"//")) + { + index = skip_line_comment(bytes, index); + last_comment_end = Some(index); + } else if bytes + .get(index..) + .is_some_and(|tail| tail.starts_with(b"/*")) + { + index = skip_block_comment(bytes, index)?; + last_comment_end = Some(index); + } else { + return last_comment_end; + } + } +} + +fn preferred_line_ending(source: &str) -> &'static str { + source + .as_bytes() + .iter() + .position(|byte| *byte == b'\n') + .filter(|index| *index > 0 && source.as_bytes()[index - 1] == b'\r') + .map_or("\n", |_| "\r\n") +} + +fn skip_line_comment(bytes: &[u8], start: usize) -> usize { + bytes[start..] + .iter() + .position(|byte| matches!(byte, b'\r' | b'\n')) + .map_or(bytes.len(), |relative| start + relative) +} + +fn skip_block_comment(bytes: &[u8], start: usize) -> Option { + if bytes.get(start..start.checked_add(2)?)? != b"/*" { + return None; + } + + let mut depth = 1usize; + let mut index = start + 2; + while index + 1 < bytes.len() { + match (bytes[index], bytes[index + 1]) { + (b'/', b'*') => { + depth = depth.checked_add(1)?; + index += 2; + } + (b'*', b'/') => { + depth -= 1; + index += 2; + if depth == 0 { + return Some(index); + } + } + _ => index += 1, + } + } + None +} + +fn insertion(offset: u32, replacement: String) -> ImportEdit { + ImportEdit { + start: offset, + end: offset, + replacement, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::Url; + + use crate::state::WorldState; + + fn plan(source: &str, target: &str, name: &str) -> Option { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + let db = world.db(); + let path = world.vfs_path_for_uri(&uri).expect("VFS path"); + let file = db.source_file(&path).expect("source file"); + let parsed = parser::parse_file_to_hir(db, file); + plan_import_edit(db, source, parsed, target, name) + } + + fn plan_module(source: &str, target: &str) -> Option { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + let db = world.db(); + let path = world.vfs_path_for_uri(&uri).expect("VFS path"); + let file = db.source_file(&path).expect("source file"); + let parsed = parser::parse_file_to_hir(db, file); + plan_module_import_edit(db, source, parsed, target) + } + + fn apply(source: &str, edit: &ImportEdit) -> String { + let start = edit.start as usize; + let end = edit.end as usize; + format!("{}{}{}", &source[..start], edit.replacement, &source[end..]) + } + + #[test] + fn appends_to_matching_selective_import() { + let source = "import {old} from lib.math;\nfunction main() { value; }\n"; + let edit = plan(source, "lib.math", "value").expect("edit"); + + assert_eq!(edit.start, edit.end); + assert_eq!(edit.replacement, ", value"); + assert_eq!( + apply(source, &edit), + "import {old, value} from lib.math;\nfunction main() { value; }\n" + ); + } + + #[test] + fn appends_after_the_last_alias() { + let source = "import {source as local} from lib;\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {source as local, value} from lib;\nfunction main() { value; }\n" + ); + } + + #[test] + fn appending_keeps_selector_comments_and_crlf_layout() { + let source = "import {old // keep old\r\n} from lib; // keep import\r\n\r\nfunction main() { value; }\r\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {old, value // keep old\r\n} from lib; // keep import\r\n\r\nfunction main() { value; }\r\n" + ); + } + + #[test] + fn appending_skips_a_nested_selector_comment() { + let source = "import {old /* outer /* inner */ still outer */} from lib;\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {old, value /* outer /* inner */ still outer */} from lib;\nfunction main() { value; }\n" + ); + } + + #[test] + fn does_not_duplicate_an_existing_unaliased_name() { + let source = "import {value} from lib;\nfunction main() { value; }\n"; + assert_eq!(plan(source, "lib", "value"), None); + } + + #[test] + fn existing_source_alias_gets_a_separate_import() { + let source = "import {value as renamed} from lib;\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {value as renamed} from lib;\nimport {value} from lib;\nfunction main() { value; }\n" + ); + } + + #[test] + fn active_alias_still_suppresses_an_ambiguous_selective_import() { + let source = "import {Other as Option} from lib;\nfunction main() { Option; }\n"; + assert_eq!(plan(source, "lib", "Option"), None); + } + + #[test] + fn namespace_and_plain_imports_get_separate_selective_imports() { + for existing in ["import * as allLib from lib;", "import * as lib from lib;"] { + let source = format!("{existing}\nfunction main() {{ value; }}\n"); + let edit = plan(&source, "lib", "value").expect("edit"); + assert_eq!( + apply(&source, &edit), + format!("{existing}\nimport {{value}} from lib;\nfunction main() {{ value; }}\n") + ); + } + } + + #[test] + fn new_import_follows_the_complete_import_block_and_keeps_blank_lines() { + let source = "import {a} from first;\nimport {b} from second; // second\n\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {a} from first;\nimport {b} from second; // second\nimport {value} from lib;\n\nfunction main() { value; }\n" + ); + } + + #[test] + fn new_import_does_not_split_a_multiline_trailing_block_comment() { + let source = + "import {a} from first; /* trailing\n block */\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {a} from first; /* trailing\n block */\nimport {value} from lib;\nfunction main() { value; }\n" + ); + } + + #[test] + fn new_import_does_not_split_a_nested_trailing_block_comment() { + let source = "import {a} from first; /* outer\n /* inner */\n still outer */\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {a} from first; /* outer\n /* inner */\n still outer */\nimport {value} from lib;\nfunction main() { value; }\n" + ); + } + + #[test] + fn new_import_preserves_crlf_and_trailing_line_comment() { + let source = "import {a} from first; // first\r\n\r\nfunction main() { value; }\r\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import {a} from first; // first\r\nimport {value} from lib;\r\n\r\nfunction main() { value; }\r\n" + ); + } + + #[test] + fn new_import_follows_leading_pragmas() { + let source = + "// license\npragma solcore noPattersonCondition;\n\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "// license\npragma solcore noPattersonCondition;\nimport {value} from lib;\n\nfunction main() { value; }\n" + ); + } + + #[test] + fn new_import_follows_header_comments_without_consuming_blank_line() { + let source = "// Copyright\n/* License */\n\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "// Copyright\n/* License */\nimport {value} from lib;\n\nfunction main() { value; }\n" + ); + } + + #[test] + fn new_import_follows_a_complete_nested_header_comment() { + let source = "/* outer /* inner */ still outer */\n\nfunction main() { value; }\n"; + let edit = plan(source, "lib", "value").expect("edit"); + + assert_eq!( + apply(source, &edit), + "/* outer /* inner */ still outer */\nimport {value} from lib;\n\nfunction main() { value; }\n" + ); + } + + #[test] + fn empty_source_gets_a_top_level_import() { + let edit = plan("", "lib.math", "value").expect("edit"); + assert_eq!( + edit, + insertion(0, "import {value} from lib.math;\n".to_owned()) + ); + } + + #[test] + fn import_at_eof_stays_on_its_own_line() { + let source = "import {a} from first; // first"; + let edit = plan(source, "lib", "value").expect("edit"); + assert_eq!( + apply(source, &edit), + "import {a} from first; // first\nimport {value} from lib;" + ); + } + + #[test] + fn supports_external_import_paths() { + let source = "function main() { value; }\n"; + let edit = plan(source, "@dep.util", "value").expect("edit"); + assert_eq!( + apply(source, &edit), + "import {value} from @dep.util;\nfunction main() { value; }\n" + ); + } + + #[test] + fn plans_a_leaf_named_namespace_import() { + let source = "function main() { math.value; }\n"; + let edit = plan_module(source, "lib.math").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import * as math from lib.math;\nfunction main() { math.value; }\n" + ); + } + + #[test] + fn identical_namespace_import_needs_no_edit() { + let source = "import * as math from lib.math; // already imported\nfunction main() { math.value; }\n"; + assert_eq!(plan_module(source, "lib.math"), None); + } + + #[test] + fn bare_selective_and_differently_aliased_imports_do_not_count_as_namespace() { + for existing in [ + "import {value} from lib.math;", + "import lib.math;", + "import * as Math from lib.math;", + ] { + let source = format!("{existing}\nfunction main() {{ math.value; }}\n"); + let edit = + plan_module(&source, "lib.math").unwrap_or_else(|| panic!("edit for {existing}")); + assert_eq!( + apply(&source, &edit), + format!( + "{existing}\nimport * as math from lib.math;\nfunction main() {{ math.value; }}\n" + ) + ); + } + } + + #[test] + fn namespace_import_preserves_crlf_after_nested_trailing_comment() { + let source = "import * as first from first; /* outer\r\n /* inner */\r\n still outer */\r\n\r\nfunction main() { math.value; }\r\n"; + let edit = plan_module(source, "lib.math").expect("edit"); + + assert_eq!( + apply(source, &edit), + "import * as first from first; /* outer\r\n /* inner */\r\n still outer */\r\nimport * as math from lib.math;\r\n\r\nfunction main() { math.value; }\r\n" + ); + } + + #[test] + fn namespace_import_follows_nested_header_comments() { + let source = + "/* license /* generated detail */ remains */\n\nfunction main() { math.value; }\n"; + let edit = plan_module(source, "@dep.math").expect("edit"); + + assert_eq!( + apply(source, &edit), + "/* license /* generated detail */ remains */\nimport * as math from @dep.math;\n\nfunction main() { math.value; }\n" + ); + } + + #[test] + fn namespace_import_rejects_invalid_paths_and_stale_metadata() { + let source = "function main() { math.value; }\n"; + assert_eq!(plan_module(source, "lib; export secret"), None); + assert_eq!(plan_module(source, ""), None); + + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + let db = world.db(); + let path = world.vfs_path_for_uri(&uri).expect("VFS path"); + let file = db.source_file(&path).expect("source file"); + let parsed = parser::parse_file_to_hir(db, file); + + assert_eq!( + plan_module_import_edit(db, "function main() {}\n", parsed, "lib.math"), + None + ); + } + + #[test] + fn rejects_malformed_source_and_invalid_generated_syntax() { + assert_eq!(plan("import {value", "lib", "value"), None); + assert_eq!( + plan("function main() {}\n", "lib; export secret", "value"), + None + ); + assert_eq!(plan("function main() {}\n", "lib", "two names"), None); + assert_eq!(plan("function main() {}\n", "", "value"), None); + } + + #[test] + fn rejects_stale_parse_metadata() { + let source = "function main() { value; }\n"; + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + let db = world.db(); + let path = world.vfs_path_for_uri(&uri).expect("VFS path"); + let file = db.source_file(&path).expect("source file"); + let parsed = parser::parse_file_to_hir(db, file); + + assert_eq!( + plan_import_edit(db, "function main() {}\n", parsed, "lib", "value"), + None + ); + } +} diff --git a/crates/lsp/src/inlay_hints.rs b/crates/lsp/src/inlay_hints.rs new file mode 100644 index 00000000..be0003ba --- /dev/null +++ b/crates/lsp/src/inlay_hints.rs @@ -0,0 +1,326 @@ +//! Inlay hint support over the wasm-clean LSP core. + +use hir::{ + anchor::DefId, + ast::{ + function::{FuncBody, FuncParam, StmtKind}, + item::{ContractItem, FunctionDef, Item, Module}, + }, + input::SourceFile, + nameres::{self as hir_nameres, TypeVarBinding}, + span::Spanned, +}; +use hir_ty::{InferResultExt, InferenceResult}; +use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Range, Url}; + +use crate::{resolve::module_id_for_uri, state::WorldState}; + +/// Computes inferred-type inlay hints for local bindings in a source range. +pub fn handle_inlay_hints(world: &WorldState, uri: &Url, range: Range) -> Option> { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let range_start = line_index.position_to_byte(range.start)?; + let range_end = line_index.position_to_byte(range.end)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + let scope = hir_nameres::item_scope_facts(db, module); + let item_facts = hir_nameres::resolve_item_type_facts_with_imports(db, module, &scope, &env); + + let mut hints = Vec::new(); + for owner in function_bodies(db, module) { + let inferred = infer_function_body(db, module, current_module, &env, &item_facts, &owner); + LetHintCollector { + db, + file, + line_index, + range_start, + range_end, + inference: &inferred, + hints: &mut hints, + } + .collect(owner.root_body); + } + + hints.sort_by_key(|hint| (hint.position.line, hint.position.character)); + Some(hints) +} + +struct FunctionBodyOwner<'db> { + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, +} + +fn function_bodies<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, +) -> Vec> { + let mut owners = Vec::new(); + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + push_function_owner(db, function, None, Vec::new(), &mut owners); + } + Item::ContractDef(contract) => { + let inherited = hir_nameres::type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + ); + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item { + push_function_owner( + db, + function, + Some(contract.def_id_value(db)), + inherited.clone(), + &mut owners, + ); + } + } + } + Item::InstanceDef(instance) => { + let inherited = hir_nameres::type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + ); + for function in instance.methods(db) { + push_function_owner(db, *function, None, inherited.clone(), &mut owners); + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + owners +} + +fn push_function_owner<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, + owners: &mut Vec>, +) { + if let Some(root_body) = function.body(db) { + owners.push(FunctionBodyOwner { + function, + root_body, + enclosing_contract, + inherited_type_vars, + }); + } +} + +fn infer_function_body<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + entry: nameres::ModuleId<'db>, + imports: &dyn hir_nameres::ImportedNames<'db>, + item_facts: &hir_nameres::ItemResolutionFacts<'db>, + owner: &FunctionBodyOwner<'db>, +) -> InferenceResult<'db> { + let sig = owner.function.sig(db); + let mut type_vars = owner.inherited_type_vars.clone(); + type_vars.extend(hir_nameres::type_var_bindings( + owner.function.def_id_value(db), + &sig.type_vars, + )); + + let body_context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract: owner.enclosing_contract, + params: hir_nameres::param_bindings(sig.params.atom()), + type_vars: type_vars.clone(), + }; + let body_map = hir_nameres::resolve_body_with_imports_and_policy( + db, + owner.root_body, + &body_context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ); + let lowered = hir_ty::lower_normalized_function_with_inferred_signature( + db, + module, + item_facts, + owner.function, + &type_vars, + Some(&body_map), + Some(entry), + ); + let param_names = sig + .params + .atom() + .iter() + .filter_map(|param| match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some(name.atom().text(db).to_owned()) + } + FuncParam::Error { .. } => None, + }) + .collect::>(); + let ty_context = hir_ty::BodyTyContext::new( + module, + body_map, + type_vars, + lowered.params.clone(), + Some(lowered.ret), + ) + .with_param_names(param_names) + .with_entry_module(entry) + .with_pre_typeck_desugar(hir_ty::pre_typeck_desugar_body_tree(db, owner.root_body)); + + hir_ty::infer_body(db, owner.root_body, ty_context) +} + +struct LetHintCollector<'a, 'db> { + db: &'db dyn hir_ty::Db, + file: SourceFile, + line_index: &'a crate::LineIndexExt, + range_start: u32, + range_end: u32, + inference: &'a InferenceResult<'db>, + hints: &'a mut Vec, +} + +impl<'db> LetHintCollector<'_, 'db> { + fn collect(&mut self, body: FuncBody<'db>) { + self.collect_body_lets(body); + + for (_, expr) in body.exprs(self.db).iter() { + if let hir::ast::function::ExprKind::Lambda { + body: lambda_body, .. + } = &expr.kind + { + self.collect(*lambda_body); + } + } + } + + fn collect_body_lets(&mut self, body: FuncBody<'db>) { + for (stmt_id, stmt) in body.stmts(self.db).iter() { + let StmtKind::Let { name, ty: None, .. } = &stmt.kind else { + continue; + }; + let absolute = name.span(self.db).resolve_to_absolute(self.db); + if absolute.file() != self.file + || absolute.start().as_u32() < self.range_start + || self.range_end < absolute.end().as_u32() + { + continue; + } + let Some(ty) = self.inference.let_ty(body, stmt_id) else { + continue; + }; + self.hints.push(type_hint( + self.line_index, + absolute.end().as_u32(), + ty.display(self.db), + )); + } + } +} + +fn type_hint(line_index: &crate::LineIndexExt, offset: u32, ty: String) -> InlayHint { + InlayHint { + position: line_index.byte_to_position(offset), + label: InlayHintLabel::String(format!(": {ty}")), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + } +} + +#[cfg(test)] +mod tests { + use lsp_types::Position; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn unannotated_let_gets_type_hint() { + let source = "function main() returns (word) {\n let x = 42;\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let range = line_index.range(0, source.len() as u32); + + let hints = handle_inlay_hints(&world, &uri, range).expect("inlay hints"); + + assert_eq!(hints.len(), 1, "expected one hint, got {hints:#?}"); + let hint = &hints[0]; + let x_offset = source.find("x = 42").expect("binding") as u32; + assert_eq!(hint.position, line_index.byte_to_position(x_offset + 1)); + assert!(hint.kind == Some(InlayHintKind::TYPE)); + let label = label_text(hint); + assert!( + label.starts_with(':'), + "expected label to start with ':', got {label}" + ); + assert!( + label.contains("word"), + "expected word type in hint label, got {label}" + ); + assert_eq!(hint.padding_left, Some(false)); + assert_eq!(hint.padding_right, Some(false)); + } + + #[test] + fn annotated_let_gets_no_type_hint() { + let source = "function main() returns (word) {\n let y: word = 42;\n return y;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let range = line_index.range(0, source.len() as u32); + + let hints = handle_inlay_hints(&world, &uri, range).expect("inlay hints"); + + assert!(hints.is_empty(), "expected no hints, got {hints:#?}"); + } + + #[test] + fn range_filters_binding_names() { + let source = "\ +function main() returns (word) { + let a = 1; + let b = 2; + return b; +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let start = line_index.byte_to_position(source.find("let b").expect("let b") as u32); + let end = Position::new(start.line + 1, 0); + + let hints = handle_inlay_hints(&world, &uri, Range::new(start, end)).expect("inlay hints"); + + assert_eq!(hints.len(), 1, "expected one ranged hint, got {hints:#?}"); + assert_eq!(label_text(&hints[0]), ": word"); + let b_offset = source.find("b = 2").expect("binding") as u32; + assert_eq!(hints[0].position, line_index.byte_to_position(b_offset + 1)); + } + + fn label_text(hint: &InlayHint) -> &str { + match &hint.label { + InlayHintLabel::String(value) => value, + other => panic!("expected string label, got {other:?}"), + } + } +} diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs new file mode 100644 index 00000000..b6bfdd27 --- /dev/null +++ b/crates/lsp/src/lib.rs @@ -0,0 +1,53 @@ +//! WASM-clean Language Server Protocol core for Solcore. +//! +//! Transport-independent state and request handlers are shared by the native +//! `tower-lsp` stdio server and the browser Web Worker binding. + +mod analysis; +pub mod capabilities; +pub mod code_actions; +pub mod completion; +pub mod definition; +pub mod diagnostics; +pub mod document_highlight; +pub mod folding; +pub mod formatting; +pub mod hover; +mod import_edits; +pub mod inlay_hints; +pub mod line_index; +#[cfg(feature = "native")] +pub mod native; +pub mod references; +pub mod rename; +mod resolve; +pub mod selection_range; +pub mod semantic_tokens; +pub mod signature_help; +pub mod state; +pub mod symbols; +#[cfg(feature = "wasm")] +pub mod wasm; +#[cfg(all(test, not(feature = "wasm")))] +mod wasm; +pub mod workspace_symbols; + +pub use capabilities::{initialize_result, server_capabilities}; +pub use code_actions::handle_code_action; +pub use completion::handle_completion; +pub use definition::handle_definition; +pub use diagnostics::compute_diagnostics; +pub use document_highlight::handle_document_highlight; +pub use folding::handle_folding_range; +pub use formatting::handle_formatting; +pub use hover::handle_hover; +pub use inlay_hints::handle_inlay_hints; +pub use line_index::LineIndexExt; +pub use references::handle_references; +pub use rename::{handle_prepare_rename, handle_rename}; +pub use selection_range::handle_selection_range; +pub use semantic_tokens::handle_semantic_tokens_full; +pub use signature_help::handle_signature_help; +pub use state::{DocumentState, WorldState, uri_to_vfs_path, vfs_url_to_client_uri}; +pub use symbols::handle_document_symbol; +pub use workspace_symbols::handle_workspace_symbol; diff --git a/crates/lsp/src/line_index.rs b/crates/lsp/src/line_index.rs new file mode 100644 index 00000000..a5874714 --- /dev/null +++ b/crates/lsp/src/line_index.rs @@ -0,0 +1,185 @@ +//! UTF-8 byte offset to LSP UTF-16 position mapping. +//! +//! Solcore compiler spans use UTF-8 byte offsets while LSP positions default +//! to UTF-16 code units. This module wraps rust-analyzer's `line-index` crate +//! so all protocol adapters share the same conversion rules. + +use line_index::{LineCol, LineIndex, TextSize, WideEncoding, WideLineCol}; +use lsp_types::{Position, Range}; + +/// Per-document position mapper. +#[derive(Debug, Clone)] +pub struct LineIndexExt { + index: LineIndex, + len: u32, + text: Box, +} + +impl LineIndexExt { + /// Builds a line index for `text`. + pub fn new(text: &str) -> Self { + Self { + index: LineIndex::new(text), + len: u32::try_from(text.len()).unwrap_or(u32::MAX), + text: text.into(), + } + } + + /// Returns the document text this index was built from. + pub fn text(&self) -> &str { + &self.text + } + + /// Converts a UTF-8 byte offset to an LSP UTF-16 position. + /// + /// Offsets are clamped to the document length. Compiler spans are expected + /// to be valid UTF-8 boundaries; if a non-boundary offset is supplied, this + /// falls back to the byte column rather than panicking. + pub fn byte_to_position(&self, offset: u32) -> Position { + let offset = TextSize::new(offset.min(self.len)); + let line_col = self.index.line_col(offset); + let wide = self + .index + .to_wide(WideEncoding::Utf16, line_col) + .unwrap_or(WideLineCol { + line: line_col.line, + col: line_col.col, + }); + + Position::new(wide.line, wide.col) + } + + /// Converts an LSP UTF-16 position to a UTF-8 byte offset. + /// + /// Returns `None` when the position is out of range or lands inside a + /// multi-byte character (e.g. the middle of a UTF-16 surrogate pair), so + /// callers never receive a byte offset that is not a UTF-8 char boundary. + pub fn position_to_byte(&self, position: Position) -> Option { + let wide = WideLineCol { + line: position.line, + col: position.character, + }; + let line_col = self.index.to_utf8(WideEncoding::Utf16, wide)?; + let offset = u32::from(self.index.offset(line_col)?); + self.text + .is_char_boundary(offset as usize) + .then_some(offset) + } + + /// Converts a UTF-8 byte range to an LSP UTF-16 range. + pub fn range(&self, start: u32, end: u32) -> Range { + Range::new(self.byte_to_position(start), self.byte_to_position(end)) + } + + /// Returns the underlying UTF-8 line/column for tests and future features. + pub fn line_col(&self, offset: u32) -> LineCol { + self.index.line_col(TextSize::new(offset.min(self.len))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_ascii_positions() { + let index = LineIndexExt::new("abc\nxy"); + + assert_eq!(index.byte_to_position(0), Position::new(0, 0)); + assert_eq!(index.byte_to_position(3), Position::new(0, 3)); + assert_eq!(index.byte_to_position(4), Position::new(1, 0)); + assert_eq!(index.byte_to_position(6), Position::new(1, 2)); + + assert_eq!(index.position_to_byte(Position::new(0, 0)), Some(0)); + assert_eq!(index.position_to_byte(Position::new(0, 3)), Some(3)); + assert_eq!(index.position_to_byte(Position::new(1, 0)), Some(4)); + assert_eq!(index.position_to_byte(Position::new(1, 2)), Some(6)); + } + + #[test] + fn maps_two_byte_character() { + let text = "aéz"; + let index = LineIndexExt::new(text); + let composed = text.find('é').expect("composed e acute") as u32; + + assert_eq!(index.byte_to_position(composed), Position::new(0, 1)); + assert_eq!( + index.byte_to_position(composed + "é".len() as u32), + Position::new(0, 2) + ); + assert_eq!(index.position_to_byte(Position::new(0, 1)), Some(composed)); + assert_eq!( + index.position_to_byte(Position::new(0, 2)), + Some(composed + "é".len() as u32) + ); + } + + #[test] + fn maps_three_byte_character() { + let text = "aあb"; + let index = LineIndexExt::new(text); + let cjk = text.find('あ').expect("cjk character") as u32; + + assert_eq!(index.byte_to_position(cjk), Position::new(0, 1)); + assert_eq!( + index.byte_to_position(cjk + "あ".len() as u32), + Position::new(0, 2) + ); + assert_eq!(index.position_to_byte(Position::new(0, 1)), Some(cjk)); + assert_eq!( + index.position_to_byte(Position::new(0, 2)), + Some(cjk + "あ".len() as u32) + ); + } + + #[test] + fn maps_four_byte_character_as_two_utf16_units() { + let text = "😀"; + let index = LineIndexExt::new(text); + + assert_eq!(index.byte_to_position(0), Position::new(0, 0)); + assert_eq!( + index.byte_to_position("😀".len() as u32), + Position::new(0, 2) + ); + assert_eq!(index.position_to_byte(Position::new(0, 0)), Some(0)); + assert_eq!( + index.position_to_byte(Position::new(0, 2)), + Some("😀".len() as u32) + ); + assert_eq!(index.position_to_byte(Position::new(0, 1)), None); + } + + #[test] + fn maps_multibyte_multiple_lines_round_trip() { + let text = "let x = \"café\";\n😀"; + let index = LineIndexExt::new(text); + let e_acute = text.find('é').expect("e acute") as u32; + let emoji = text.find('😀').expect("emoji") as u32; + + assert_eq!(e_acute, 12); + assert_eq!(emoji, 17); + assert_eq!(index.byte_to_position(e_acute), Position::new(0, 12)); + assert_eq!( + index.byte_to_position(e_acute + "é".len() as u32), + Position::new(0, 13) + ); + assert_eq!(index.byte_to_position(emoji), Position::new(1, 0)); + assert_eq!( + index.byte_to_position(emoji + "😀".len() as u32), + Position::new(1, 2) + ); + + for offset in [ + 0, + e_acute, + e_acute + "é".len() as u32, + emoji, + text.len() as u32, + ] { + let position = index.byte_to_position(offset); + assert_eq!(index.position_to_byte(position), Some(offset)); + } + assert_eq!(index.position_to_byte(Position::new(1, 1)), None); + } +} diff --git a/crates/lsp/src/native.rs b/crates/lsp/src/native.rs new file mode 100644 index 00000000..868dc7cc --- /dev/null +++ b/crates/lsp/src/native.rs @@ -0,0 +1,627 @@ +//! Native stdio transport for the Solcore language server. + +use std::{ + fs, + path::Path, + sync::atomic::{AtomicBool, Ordering}, +}; + +use lsp_types::{ + CodeActionParams, CodeActionResponse, CompletionParams, CompletionResponse, Diagnostic, + DidChangeTextDocumentParams, DidChangeWatchedFilesParams, + DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersParams, + DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams, + DocumentHighlight, DocumentHighlightParams, DocumentSymbolParams, DocumentSymbolResponse, + FileChangeType, FileSystemWatcher, FoldingRange, FoldingRangeParams, GlobPattern, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, InitializeParams, + InitializeResult, InitializedParams, InlayHint, InlayHintParams, Location, MessageType, + PrepareRenameResponse, ReferenceParams, Registration, RenameParams, SelectionRange, + SelectionRangeParams, SemanticTokensParams, SemanticTokensResult, SignatureHelp, + SignatureHelpParams, SymbolInformation, TextDocumentPositionParams, TextEdit, Url, WatchKind, + WorkspaceEdit, WorkspaceSymbolParams, +}; +use tokio::sync::Mutex; +use tower_lsp::{Client, LanguageServer, LspService, Server, jsonrpc}; + +/// Tower-LSP backend over the transport-independent Solcore LSP core. +pub struct Backend { + client: Client, + world: Mutex, + document_updates: Mutex<()>, + supports_dynamic_file_watching: AtomicBool, +} + +impl Backend { + /// Creates a backend for a connected LSP client. + pub fn new(client: Client) -> Self { + Self { + client, + world: Mutex::new(crate::state::WorldState::new()), + document_updates: Mutex::new(()), + supports_dynamic_file_watching: AtomicBool::new(false), + } + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { + self.supports_dynamic_file_watching.store( + params + .capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files) + .and_then(|watched| watched.dynamic_registration) + .unwrap_or(false), + Ordering::Relaxed, + ); + let roots = initial_workspace_roots(¶ms) + .into_iter() + .map(|root| { + let files = read_workspace_documents(&root); + (root, files) + }) + .collect::>(); + if !roots.is_empty() { + self.world.lock().await.load_workspace_roots(roots); + } + Ok(crate::capabilities::initialize_result()) + } + + async fn initialized(&self, _: InitializedParams) { + if self.supports_dynamic_file_watching.load(Ordering::Relaxed) + && let Err(error) = self + .client + .register_capability(vec![watched_files_registration()]) + .await + { + self.client + .log_message( + MessageType::WARNING, + format!("failed to register Solcore file watcher: {error}"), + ) + .await; + } + let file_count = self.world.lock().await.workspace_document_uris().len(); + self.client + .log_message( + MessageType::INFO, + format!("solcore-lsp ready ({file_count} workspace files loaded)"), + ) + .await; + } + + async fn shutdown(&self) -> jsonrpc::Result<()> { + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let _update = self.document_updates.lock().await; + let uri = params.text_document.uri; + let version = params.text_document.version; + let text = params.text_document.text; + + let infer_workspace_root = { + let world = self.world.lock().await; + !world.has_workspace_root() + && world.vfs_path_for_uri(&uri).is_none() + && uri.scheme() == "file" + }; + let inferred_workspace = infer_workspace_root + .then(|| workspace_for_document(&uri)) + .flatten(); + + let diagnostics = { + let mut world = self.world.lock().await; + if let Some((root, files)) = inferred_workspace { + world.load_workspace_documents(root, files); + } + world.open_document(uri.clone(), text); + diagnostics_with_versions(&world, Some((&uri, version))) + }; + + publish_diagnostics(&self.client, diagnostics).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let _update = self.document_updates.lock().await; + let uri = params.text_document.uri; + let version = params.text_document.version; + if params.content_changes.is_empty() { + return; + } + + let diagnostics = { + let mut world = self.world.lock().await; + world + .apply_document_changes(&uri, params.content_changes) + .then(|| diagnostics_with_versions(&world, Some((&uri, version)))) + }; + let Some(diagnostics) = diagnostics else { + self.client + .log_message( + MessageType::ERROR, + format!("ignored invalid content change for {uri}"), + ) + .await; + return; + }; + + publish_diagnostics(&self.client, diagnostics).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let _update = self.document_updates.lock().await; + let uri = params.text_document.uri; + let disk_text = read_document(&uri); + + let diagnostics = { + let mut world = self.world.lock().await; + let belongs_to_workspace = world.is_uri_in_workspace(&uri); + world.close_document(&uri); + match (belongs_to_workspace, disk_text) { + (true, Some(text)) => { + world.set_workspace_document(uri.clone(), text); + } + (false, _) | (true, None) => { + world.remove_workspace_document(&uri); + } + } + diagnostics_with_versions(&world, None) + }; + + self.client.publish_diagnostics(uri, vec![], None).await; + publish_diagnostics(&self.client, diagnostics).await; + } + + async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { + let _update = self.document_updates.lock().await; + let removed = params + .event + .removed + .into_iter() + .map(|folder| folder.uri) + .collect::>(); + let added = params + .event + .added + .into_iter() + .map(|folder| { + let files = read_workspace_documents(&folder.uri); + (folder.uri, files) + }) + .collect::>(); + + let (diagnostics, discarded, root_count, file_count) = { + let mut world = self.world.lock().await; + let (_, discarded) = world.update_workspace_roots(removed, added); + ( + diagnostics_with_versions(&world, None), + discarded, + world.workspace_root_count(), + world.workspace_document_uris().len(), + ) + }; + + for uri in discarded { + self.client.publish_diagnostics(uri, Vec::new(), None).await; + } + publish_diagnostics(&self.client, diagnostics).await; + self.client + .log_message( + MessageType::INFO, + format!( + "workspace folders updated ({root_count} roots, {file_count} files loaded)" + ), + ) + .await; + } + + async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { + let _update = self.document_updates.lock().await; + let changes = params + .changes + .into_iter() + .filter(|event| is_solcore_uri(&event.uri)) + .map(|event| { + let text = (event.typ != FileChangeType::DELETED) + .then(|| read_document(&event.uri)) + .flatten(); + (event.uri, event.typ, text) + }) + .collect::>(); + + let diagnostics = { + let mut world = self.world.lock().await; + for (uri, kind, text) in changes { + if world.is_document_open(&uri) { + continue; + } + if kind == FileChangeType::DELETED { + world.remove_workspace_document(&uri); + } else if let Some(text) = text { + world.set_workspace_document(uri, text); + } + } + diagnostics_with_versions(&world, None) + }; + + publish_diagnostics(&self.client, diagnostics).await; + } + + async fn completion( + &self, + params: CompletionParams, + ) -> jsonrpc::Result> { + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let world = self.world.lock().await; + + Ok(crate::completion::handle_completion(&world, &uri, position)) + } + + async fn hover(&self, params: HoverParams) -> jsonrpc::Result> { + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let world = self.world.lock().await; + + Ok(crate::hover::handle_hover(&world, &uri, position)) + } + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> jsonrpc::Result> { + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let world = self.world.lock().await; + + Ok(crate::definition::handle_definition(&world, &uri, position)) + } + + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> jsonrpc::Result> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::symbols::handle_document_symbol(&world, &uri)) + } + + async fn code_action( + &self, + params: CodeActionParams, + ) -> jsonrpc::Result> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::code_actions::handle_code_action( + &world, + &uri, + params.range, + ¶ms.context, + )) + } + + async fn formatting( + &self, + params: DocumentFormattingParams, + ) -> jsonrpc::Result>> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::formatting::handle_formatting( + &world, + &uri, + ¶ms.options, + )) + } + + async fn folding_range( + &self, + params: FoldingRangeParams, + ) -> jsonrpc::Result>> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::folding::handle_folding_range(&world, &uri)) + } + + async fn selection_range( + &self, + params: SelectionRangeParams, + ) -> jsonrpc::Result>> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::selection_range::handle_selection_range( + &world, + &uri, + ¶ms.positions, + )) + } + + async fn references(&self, params: ReferenceParams) -> jsonrpc::Result>> { + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let include_declaration = params.context.include_declaration; + let world = self.world.lock().await; + + Ok(crate::references::handle_references( + &world, + &uri, + position, + include_declaration, + )) + } + + async fn document_highlight( + &self, + params: DocumentHighlightParams, + ) -> jsonrpc::Result>> { + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let world = self.world.lock().await; + + Ok(crate::document_highlight::handle_document_highlight( + &world, &uri, position, + )) + } + + async fn rename(&self, params: RenameParams) -> jsonrpc::Result> { + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let world = self.world.lock().await; + + Ok(crate::rename::handle_rename( + &world, + &uri, + position, + ¶ms.new_name, + )) + } + + async fn prepare_rename( + &self, + params: TextDocumentPositionParams, + ) -> jsonrpc::Result> { + let uri = params.text_document.uri; + let position = params.position; + let world = self.world.lock().await; + + Ok(crate::rename::handle_prepare_rename(&world, &uri, position)) + } + + async fn signature_help( + &self, + params: SignatureHelpParams, + ) -> jsonrpc::Result> { + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let world = self.world.lock().await; + + Ok(crate::signature_help::handle_signature_help( + &world, &uri, position, + )) + } + + async fn semantic_tokens_full( + &self, + params: SemanticTokensParams, + ) -> jsonrpc::Result> { + let uri = params.text_document.uri; + let world = self.world.lock().await; + + Ok(crate::semantic_tokens::handle_semantic_tokens_full( + &world, &uri, + )) + } + + async fn inlay_hint(&self, params: InlayHintParams) -> jsonrpc::Result>> { + let uri = params.text_document.uri; + let range = params.range; + let world = self.world.lock().await; + + Ok(crate::inlay_hints::handle_inlay_hints(&world, &uri, range)) + } + + async fn symbol( + &self, + params: WorkspaceSymbolParams, + ) -> jsonrpc::Result>> { + let world = self.world.lock().await; + + Ok(crate::workspace_symbols::handle_workspace_symbol( + &world, + ¶ms.query, + )) + } +} + +type DiagnosticsBatch = Vec<(Url, Vec, Option)>; + +fn diagnostics_with_versions( + world: &crate::state::WorldState, + versioned_uri: Option<(&Url, i32)>, +) -> DiagnosticsBatch { + crate::diagnostics::compute_open_document_diagnostics(world) + .into_iter() + .map(|(uri, diagnostics)| { + let version = versioned_uri + .and_then(|(versioned_uri, version)| (versioned_uri == &uri).then_some(version)); + (uri, diagnostics, version) + }) + .collect() +} + +async fn publish_diagnostics(client: &Client, diagnostics: DiagnosticsBatch) { + for (uri, diagnostics, version) in diagnostics { + client.publish_diagnostics(uri, diagnostics, version).await; + } +} + +#[allow(deprecated)] +fn initial_workspace_roots(params: &InitializeParams) -> Vec { + let workspace_folders = params + .workspace_folders + .as_ref() + .map(|folders| { + folders + .iter() + .map(|folder| folder.uri.clone()) + .collect::>() + }) + .filter(|folders| !folders.is_empty()); + workspace_folders.unwrap_or_else(|| { + params + .root_uri + .clone() + .or_else(|| { + params + .root_path + .as_ref() + .and_then(|path| Url::from_directory_path(path).ok()) + }) + .into_iter() + .collect() + }) +} + +fn watched_files_registration() -> Registration { + let options = DidChangeWatchedFilesRegistrationOptions { + watchers: vec![FileSystemWatcher { + glob_pattern: GlobPattern::String("**/*.solc".to_owned()), + kind: Some(WatchKind::Create | WatchKind::Change | WatchKind::Delete), + }], + }; + Registration { + id: "solcore-watch-solc".to_owned(), + method: "workspace/didChangeWatchedFiles".to_owned(), + register_options: serde_json::to_value(options).ok(), + } +} + +fn workspace_for_document(uri: &Url) -> Option<(Url, Vec<(Url, String)>)> { + let path = uri.to_file_path().ok()?; + let root = Url::from_directory_path(path.parent()?).ok()?; + let files = read_workspace_documents(&root); + Some((root, files)) +} + +fn read_workspace_documents(root: &Url) -> Vec<(Url, String)> { + let Ok(root) = root.to_file_path() else { + return Vec::new(); + }; + let mut directories = vec![root]; + let mut documents = Vec::new(); + + while let Some(directory) = directories.pop() { + let Ok(entries) = fs::read_dir(directory) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + if file_type.is_symlink() { + continue; + } + if file_type.is_dir() { + if !is_ignored_directory(&path) { + directories.push(path); + } + continue; + } + if !is_solcore_path(&path) { + continue; + } + let Ok(text) = fs::read_to_string(&path) else { + continue; + }; + let Ok(uri) = Url::from_file_path(&path) else { + continue; + }; + documents.push((uri, text)); + } + } + + documents.sort_by(|left, right| left.0.as_str().cmp(right.0.as_str())); + documents +} + +fn read_document(uri: &Url) -> Option { + let path = uri.to_file_path().ok()?; + is_solcore_path(&path) + .then(|| fs::read_to_string(path).ok()) + .flatten() +} + +fn is_solcore_uri(uri: &Url) -> bool { + uri.to_file_path() + .ok() + .is_some_and(|path| is_solcore_path(&path)) +} + +fn is_solcore_path(path: &Path) -> bool { + path.extension().and_then(|extension| extension.to_str()) == Some("solc") +} + +fn is_ignored_directory(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| matches!(name, ".git" | ".hg" | ".svn" | "node_modules" | "target")) +} + +/// Runs the native language server over process stdin/stdout. +pub async fn run_stdio() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + let (service, socket) = LspService::new(Backend::new); + Server::new(stdin, stdout, socket).serve(service).await; +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::WorkspaceFolder; + + #[allow(deprecated)] + #[test] + fn initialize_prefers_every_workspace_folder_over_legacy_root() { + let left = Url::parse("file:///workspace/left").expect("left root"); + let right = Url::parse("file:///workspace/right").expect("right root"); + let legacy = Url::parse("file:///workspace/legacy").expect("legacy root"); + let params = InitializeParams { + root_uri: Some(legacy), + workspace_folders: Some(vec![ + WorkspaceFolder { + uri: left.clone(), + name: "left".to_owned(), + }, + WorkspaceFolder { + uri: right.clone(), + name: "right".to_owned(), + }, + ]), + ..InitializeParams::default() + }; + + assert_eq!(initial_workspace_roots(¶ms), vec![left, right]); + } + + #[allow(deprecated)] + #[test] + fn initialize_falls_back_to_root_uri_for_empty_workspace_folders() { + let root = Url::parse("file:///workspace/project").expect("root uri"); + let params = InitializeParams { + root_uri: Some(root.clone()), + workspace_folders: Some(Vec::new()), + ..InitializeParams::default() + }; + + assert_eq!(initial_workspace_roots(¶ms), vec![root]); + } +} diff --git a/crates/lsp/src/references.rs b/crates/lsp/src/references.rs new file mode 100644 index 00000000..7642e9b1 --- /dev/null +++ b/crates/lsp/src/references.rs @@ -0,0 +1,2678 @@ +//! Find-references support over the wasm-clean LSP core. + +use std::collections::HashSet; + +use hir::{ + anchor::{DefId, resolve_def_location}, + ast::{ + function::{Expr, ExprKind, FuncBody, FuncParam, Pat, PatCtorHead, PatKind, StmtKind}, + item::{ + AdtDef, ClassDef, ConstructorSelector, ContractDef, ContractItem, ExportKind, + ExportedName, FunctionDef, ImportSelector, Item, Module, SelectedName, + }, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + diag::{AbsoluteSpan, Offset}, + input::SourceFile, + nameres::{ + self as hir_nameres, CtorIndex, FieldId, LocalBinding, ModuleRef, ParamId, ParamIndex, + Resolution, TypeVarBinding, TypeVarId, + }, + span::{Span, Spanned, SpannedElem}, +}; +use lsp_types::{Location, Position, Url}; +use nameres::Db as _; + +use crate::{ + LineIndexExt, + resolve::{function_owning_offset, innermost_expr, module_id_for_uri}, + state::WorldState, +}; + +/// Semantic identity used by references, highlights, and future rename support. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ReferenceTarget<'db> { + /// A named user definition such as a function, type, contract, trait, or + /// impl. + Def(DefId<'db>), + /// An enum constructor identified by its owning type and constructor index. + Ctor { + /// The ADT that owns the constructor. + ty: DefId<'db>, + /// The constructor's index in the ADT declaration. + index: CtorIndex, + }, + /// A function or lambda parameter. + Param(ParamId<'db>), + /// A local body binding, including pattern variables and type variables. + Local(LocalBinding<'db>), + /// A contract field. + Field(FieldId<'db>), + /// A trait method. + ClassMethod { + /// The trait that declares the method. + class: DefId<'db>, + /// The method name. + name: String, + }, + /// A module qualifier binding local to one source module. + Module(ModuleRef<'db>), + /// A local alias introduced by `import {source as alias} from m`. + ImportAlias { + /// Module definition that owns the import declaration. + owner: DefId<'db>, + /// Alias spelling used in the importing module. + name: String, + }, + /// A module alias published by `export path as Alias`. + ExportedModuleAlias { + /// Module definition that owns the export declaration. + owner: DefId<'db>, + /// Alias spelling visible to downstream modules. + name: String, + }, +} + +struct FunctionContext<'db> { + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, +} + +/// Computes all reference locations for the symbol at a source position. +pub fn handle_references( + world: &WorldState, + uri: &Url, + position: Position, + include_declaration: bool, +) -> Option> { + let target = reference_target_at(world, uri, position)?; + Some( + collect_reference_locations(world, &target, include_declaration) + .into_iter() + .filter(|location| world.line_index(&location.uri).is_some()) + .collect(), + ) +} + +/// Resolves the symbol under `position` to a reusable semantic reference +/// target. +pub fn reference_target_at<'db>( + world: &'db WorldState, + uri: &Url, + position: Position, +) -> Option> { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let module_id = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, module_id); + + if let Some(target) = body_expr_target_at(db, module, file, offset, &env) { + return Some(target); + } + + for context in function_contexts(db, module) { + if let Some(target) = function_param_target_at(db, &context, file, offset) { + return Some(target); + } + + let body_map = body_resolution_map( + db, + module, + context.function, + context.root_body, + context.enclosing_contract, + context.inherited_type_vars.clone(), + &env, + ); + if let Some(target) = body_map_target_at(db, file, offset, &body_map, &env) { + return Some(target); + } + } + + if let Some(target) = item_type_var_target_at(db, module, file, offset) { + return Some(target); + } + + let scope = hir_nameres::item_scope_facts(db, module); + let item_facts = hir_nameres::resolve_item_type_facts_with_imports(db, module, &scope, &env); + item_resolution_target_at(db, file, offset, &item_facts, &env) + .or_else(|| item_scope_target_at(db, file, offset, &scope)) + .or_else(|| import_export_target_at(world, uri, position)) +} + +/// Resolves an import selector or explicit export-list occurrence under +/// `position`. +pub fn import_export_target_at<'db>( + world: &'db WorldState, + uri: &Url, + position: Position, +) -> Option> { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let module_id = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + + import_export_target_in_module(db, module, module_id, file, offset) +} + +/// Collects all known references to `target` in reachable and open modules. +pub fn collect_reference_locations<'db>( + world: &'db WorldState, + target: &ReferenceTarget<'db>, + include_declaration: bool, +) -> Vec { + let db = world.db(); + let mut locations = Vec::new(); + + for module_id in reference_search_modules(world, db) { + let Some(file) = db.module_file(module_id) else { + continue; + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, module_id); + let scope = hir_nameres::item_scope(db, module); + let module_map = hir_nameres::resolve_module_with_imports_and_policy( + db, + module, + scope, + &env, + hir_nameres::NameresDiagnosticPolicy::Emit, + ); + + collect_item_resolution_locations( + world, + db, + &module_map.item_resolutions.facts, + &env, + target, + &mut locations, + ); + collect_import_export_reference_locations( + world, + db, + module, + module_id, + &module_map.item_scope.facts, + target, + &mut locations, + ); + for body_map in &module_map.bodies { + collect_body_reference_locations(world, db, body_map, &env, target, &mut locations); + } + } + + if include_declaration + && let Some(span) = target_declaration_span(db, target) + && let Some(location) = location_for_span(world, db, span) + { + locations.push(location); + } + + sort_dedup_locations(&mut locations); + locations +} + +/// Returns whether a semantic target can be renamed with text edits alone. +/// +/// Imported module paths name files and require a resource rename, which this +/// LSP does not perform. An explicit module alias is local source syntax and is +/// therefore safe to rename. +pub fn target_supports_text_rename(world: &WorldState, target: &ReferenceTarget<'_>) -> bool { + let db = world.db(); + match target { + ReferenceTarget::Module(module_ref) => { + let file = module_ref.owner.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + module.items(db).iter().any(|item| { + let Item::Import(import) = *item else { + return false; + }; + import + .alias_elem(db) + .is_some_and(|alias| alias.atom().text(db) == module_ref.name) + }) + } + ReferenceTarget::ImportAlias { owner, name } => !import_alias_is_exported(db, *owner, name), + ReferenceTarget::ExportedModuleAlias { .. } => { + target_declaration_span(db, target).is_some() + } + ReferenceTarget::Def(_) => { + !shared_selector_contains_target(world, target) + && !definition_has_public_alias(world, target) + } + ReferenceTarget::Ctor { .. } + | ReferenceTarget::Param(_) + | ReferenceTarget::Local(_) + | ReferenceTarget::Field(_) + | ReferenceTarget::ClassMethod { .. } => true, + } +} + +fn definition_has_public_alias(world: &WorldState, target: &ReferenceTarget<'_>) -> bool { + let ReferenceTarget::Def(def) = target else { + return false; + }; + let db = world.db(); + reference_search_modules(world, db) + .into_iter() + .any(|module_id| { + let Some(file) = db.module_file(module_id) else { + return false; + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let exported = module + .items(db) + .iter() + .filter_map(|item| { + let Item::Export(export) = *item else { + return None; + }; + let ExportKind::List(names) = export.kind(db) else { + return None; + }; + Some( + names + .iter() + .map(|name| name.name.atom().text(db).to_owned()), + ) + }) + .flatten() + .collect::>(); + if exported.is_empty() { + return false; + } + let env = nameres::module_env(db, module_id); + module.items(db).iter().any(|item| { + let Item::Import(import) = *item else { + return false; + }; + let Some(ImportSelector::Names(names)) = import.selector(db) else { + return false; + }; + names.iter().any(|selected| { + let Some(alias) = selected.alias.as_ref() else { + return false; + }; + exported.contains(alias.atom().text(db)) + && import_selected_name_resolutions(db, &env, selected) + .iter() + .any(|resolution| { + matches!( + resolution, + Resolution::Def { def: candidate, .. } if candidate == def + ) + }) + }) + }) + }) +} + +fn import_alias_is_exported(db: &dyn hir_ty::Db, owner: DefId<'_>, name: &str) -> bool { + let module = parser::parse_file_to_hir(db, owner.file(db)).module(db); + module.items(db).iter().any(|item| { + let Item::Export(export) = *item else { + return false; + }; + matches!( + export.kind(db), + ExportKind::List(names) + if names.iter().any(|exported| exported.name.atom().text(db) == name) + ) + }) +} + +fn shared_selector_contains_target(world: &WorldState, target: &ReferenceTarget<'_>) -> bool { + let db = world.db(); + reference_search_modules(world, db) + .into_iter() + .any(|module_id| { + let Some(file) = db.module_file(module_id) else { + return false; + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, module_id); + let scope = hir_nameres::item_scope_facts(db, module); + module.items(db).iter().any(|item| match *item { + Item::Import(import) => { + let Some(ImportSelector::Names(names)) = import.selector(db) else { + return false; + }; + names.iter().any(|selected| { + let targets = import_selected_name_resolutions(db, &env, selected) + .iter() + .filter_map(target_from_resolution) + .collect::>(); + targets.len() > 1 && targets.iter().any(|candidate| candidate == target) + }) + } + Item::Export(export) => match export.kind(db) { + ExportKind::List(names) => names.iter().any(|exported| { + let targets = export_name_targets(db, &scope, &env, exported); + targets.len() > 1 && targets.iter().any(|candidate| candidate == target) + }), + ExportKind::ItemsFrom(path, names) => { + let path_ref = nameres::ModulePathRef { + span: export.span(db), + external: None, + segments: path.clone(), + }; + nameres::resolve_module_path(db, module_id, path_ref) + .ok() + .is_some_and(|provider| { + names.iter().any(|exported| { + let targets = reexport_name_targets(db, provider, exported); + targets.len() > 1 + && targets.iter().any(|candidate| candidate == target) + }) + }) + } + ExportKind::Module(_) | ExportKind::ModuleAs(_, _) => false, + }, + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::ContractDef(_) + | Item::Pragma(_) + | Item::Error { .. } => false, + }) + }) +} + +fn reference_search_modules<'db>( + world: &'db WorldState, + db: &'db vfs::AnalysisHost, +) -> Vec> { + let mut modules = Vec::new(); + + if let Some(entry) = world.workspace().entry_module() { + for module in nameres::reachable_modules(db, entry) { + push_unique_module(&mut modules, module); + } + } + + for uri in world.workspace_document_uris() { + if let Some(module) = module_id_for_uri(world, db, &uri) { + push_unique_module(&mut modules, module); + } + } + + modules +} + +fn push_unique_module<'db>( + modules: &mut Vec>, + module: nameres::ModuleId<'db>, +) { + if !modules.contains(&module) { + modules.push(module); + } +} + +fn import_export_target_in_module<'db>( + db: &'db vfs::AnalysisHost, + module: Module<'db>, + module_id: nameres::ModuleId<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + let env = nameres::module_env(db, module_id); + let scope = hir_nameres::item_scope_facts(db, module); + + for item in module.items(db) { + match *item { + Item::Import(import) => { + let Some(ImportSelector::Names(names)) = import.selector(db) else { + continue; + }; + for selected in names { + if span_contains_offset(db, selected.name.span(db), file, offset) { + let mut targets = import_selected_name_resolutions(db, &env, selected) + .iter() + .filter_map(target_from_resolution) + .collect::>(); + targets.dedup(); + return (targets.len() == 1).then(|| targets.remove(0)); + } + if selected + .alias + .as_ref() + .is_some_and(|alias| span_contains_offset(db, alias.span(db), file, offset)) + { + return import_alias_target(db, module, selected); + } + if let Some(target) = constructor_selector_target_at( + db, + import_selected_type_target(db, &env, selected).as_ref(), + selected.constructors.as_ref(), + file, + offset, + ) { + return Some(target); + } + } + } + Item::Export(export) => { + let (names, reexport_module) = match export.kind(db) { + ExportKind::List(names) => (names.as_slice(), None), + ExportKind::ItemsFrom(path, names) => { + let path_ref = nameres::ModulePathRef { + span: export.span(db), + external: None, + segments: path.clone(), + }; + ( + names.as_slice(), + nameres::resolve_module_path(db, module_id, path_ref).ok(), + ) + } + ExportKind::Module(_) => continue, + ExportKind::ModuleAs(_, alias) => { + if span_contains_offset(db, alias.span(db), file, offset) { + return Some(ReferenceTarget::ExportedModuleAlias { + owner: module.def_id_value(db), + name: alias.atom().text(db).to_owned(), + }); + } + continue; + } + }; + for exported in names { + let targets = reexport_module.map_or_else( + || export_name_targets(db, &scope, &env, exported), + |target| reexport_name_targets(db, target, exported), + ); + let type_target = reexport_module + .and_then(|target| reexport_name_type_target(db, target, exported)) + .or_else(|| export_name_type_target(db, &scope, &env, exported)); + if span_contains_offset(db, exported.name.span(db), file, offset) { + return (targets.len() == 1).then(|| targets[0].clone()); + } + if let Some(target) = constructor_selector_target_at( + db, + type_target.as_ref(), + exported.constructors.as_ref(), + file, + offset, + ) { + return Some(target); + } + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::ContractDef(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn body_expr_target_at<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + file: SourceFile, + offset: u32, + env: &dyn hir_nameres::ImportedNames<'db>, +) -> Option> { + let owner = function_owning_offset(db, module, file, offset)?; + let body_map = body_resolution_map( + db, + module, + owner.function, + owner.root_body, + owner.enclosing_contract, + owner.inherited_type_vars, + env, + ); + let (owning_body, expr_id) = innermost_expr(db, owner.root_body, file, offset)?; + let expr = owning_body.exprs(db).get(expr_id); + let span = expr_reference_span(db, expr)?; + if !span_contains_offset(db, span, file, offset) { + return None; + } + + body_map + .exprs + .iter() + .find(|entry| entry.body == owning_body && entry.expr == expr_id) + .and_then(|entry| target_from_resolution_at_span(db, &entry.resolution, span)) +} + +fn function_contexts<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, +) -> Vec> { + let mut contexts = Vec::new(); + + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + push_function_context(db, function, None, Vec::new(), &mut contexts); + } + Item::ContractDef(contract) => { + let inherited = hir_nameres::type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + ); + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item { + push_function_context( + db, + function, + Some(contract.def_id_value(db)), + inherited.clone(), + &mut contexts, + ); + } + } + } + Item::InstanceDef(instance) => { + let inherited = hir_nameres::type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + ); + for function in instance.methods(db) { + push_function_context(db, *function, None, inherited.clone(), &mut contexts); + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + contexts +} + +fn push_function_context<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, + contexts: &mut Vec>, +) { + if let Some(root_body) = function.body(db) { + contexts.push(FunctionContext { + function, + root_body, + enclosing_contract, + inherited_type_vars, + }); + } +} + +fn function_param_target_at<'db>( + db: &'db dyn hir_ty::Db, + context: &FunctionContext<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + let sig = context.function.sig(db); + if let Some(target) = + param_target_in_list(db, sig.params.atom(), context.root_body, file, offset) + { + return Some(target); + } + + lambda_param_target_at(db, context.root_body, file, offset) +} + +fn lambda_param_target_at<'db>( + db: &'db dyn hir_ty::Db, + root_body: FuncBody<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + let mut stack = vec![root_body]; + while let Some(body) = stack.pop() { + for (_, expr) in body.exprs(db).iter() { + if let ExprKind::Lambda { + params, + body: lambda_body, + .. + } = &expr.kind + { + if let Some(target) = + param_target_in_list(db, params.atom(), *lambda_body, file, offset) + { + return Some(target); + } + stack.push(*lambda_body); + } + } + } + + None +} + +fn param_target_in_list<'db>( + db: &'db dyn hir_ty::Db, + params: &[FuncParam<'db>], + body: FuncBody<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + params.iter().enumerate().find_map(|(index, param)| { + let span = param_name_or_whole_span(db, param)?; + span_contains_offset(db, span, file, offset).then_some(ReferenceTarget::Param(ParamId { + body, + index: ParamIndex::from_usize(index), + })) + }) +} + +fn body_map_target_at<'db>( + db: &'db dyn hir_ty::Db, + file: SourceFile, + offset: u32, + body_map: &hir_nameres::BodyResolutionMap<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, +) -> Option> { + for entry in &body_map.stmt_bindings { + let span = stmt_binding_span(db, entry.body, entry.stmt)?; + if span_contains_offset(db, span, file, offset) { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + for entry in &body_map.pats { + let pat = entry.body.pats(db).get(entry.pat); + if let Some(qualifier) = pat_module_qualifier(pat) + && let Some(target) = module_qualifier_target_at(db, env, qualifier, file, offset) + { + return Some(target); + } + if let Some(span) = pat_reference_span(db, pat) + && span_contains_offset(db, span, file, offset) + { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + for entry in &body_map.types { + if let Some(qualifier) = type_ref_module_qualifier(db, entry.ty) + && let Some(target) = module_qualifier_target_at(db, env, qualifier, file, offset) + { + return Some(target); + } + if let Some(span) = type_ref_name_span(db, entry.ty) + && span_contains_offset(db, span, file, offset) + { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + for entry in &body_map.preds { + if let Some(span) = pred_ref_class_span(db, entry.pred) + && span_contains_offset(db, span, file, offset) + { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + None +} + +fn item_resolution_target_at<'db>( + db: &'db dyn hir_ty::Db, + file: SourceFile, + offset: u32, + item_facts: &hir_nameres::ItemResolutionFacts<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, +) -> Option> { + for entry in &item_facts.types { + if let Some(qualifier) = type_ref_module_qualifier(db, entry.ty) + && let Some(target) = module_qualifier_target_at(db, env, qualifier, file, offset) + { + return Some(target); + } + if let Some(span) = type_ref_name_span(db, entry.ty) + && span_contains_offset(db, span, file, offset) + { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + for entry in &item_facts.preds { + if let Some(span) = pred_ref_class_span(db, entry.pred) + && span_contains_offset(db, span, file, offset) + { + return target_from_resolution_at_span(db, &entry.resolution, span); + } + } + + None +} + +fn item_scope_target_at<'db>( + db: &'db dyn hir_ty::Db, + file: SourceFile, + offset: u32, + scope: &hir_nameres::ItemScopeFacts<'db>, +) -> Option> { + scope_entries_target_at(db, file, offset, &scope.types) + .or_else(|| scope_entries_target_at(db, file, offset, &scope.terms)) + .or_else(|| scope_entries_target_at(db, file, offset, &scope.modules)) + .or_else(|| ctor_lists_target_at(db, file, offset, &scope.ctor_lists)) + .or_else(|| { + scope.contracts.iter().find_map(|contract| { + scope_entries_target_at(db, file, offset, &contract.types) + .or_else(|| scope_entries_target_at(db, file, offset, &contract.terms)) + .or_else(|| { + contract.fields.iter().find_map(|field| { + span_contains_offset(db, field.span, file, offset) + .then_some(ReferenceTarget::Field(field.field)) + }) + }) + .or_else(|| ctor_lists_target_at(db, file, offset, &contract.ctor_lists)) + }) + }) +} + +fn scope_entries_target_at<'db>( + db: &'db dyn hir_ty::Db, + file: SourceFile, + offset: u32, + entries: &hir_nameres::NamespaceTable<'db>, +) -> Option> { + entries.iter().find_map(|entry| { + span_contains_offset(db, entry.span, file, offset) + .then(|| target_from_resolution_at_span(db, &entry.resolution, entry.span)) + .flatten() + }) +} + +fn ctor_lists_target_at<'db>( + db: &'db dyn hir_ty::Db, + file: SourceFile, + offset: u32, + lists: &[hir_nameres::CtorList<'db>], +) -> Option> { + lists.iter().find_map(|list| { + list.ctors.iter().find_map(|ctor| { + span_contains_offset(db, ctor.span, file, offset).then_some(ReferenceTarget::Ctor { + ty: ctor.ty, + index: ctor.index, + }) + }) + }) +} + +fn item_type_var_target_at<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + if let Some(target) = function_type_var_target_at(db, function, file, offset) { + return Some(target); + } + } + Item::TypeAlias(alias) => { + if let Some(target) = type_var_target_in_list( + db, + alias.def_id_value(db), + alias.ty_param_elems(db), + file, + offset, + ) { + return Some(target); + } + } + Item::AdtDef(adt) => { + if let Some(target) = type_var_target_in_list( + db, + adt.def_id_value(db), + adt.ty_param_elems(db), + file, + offset, + ) { + return Some(target); + } + } + Item::ClassDef(class) => { + if let Some(target) = type_var_target_in_list( + db, + class.def_id_value(db), + class.type_var_elems(db), + file, + offset, + ) { + return Some(target); + } + } + Item::InstanceDef(instance) => { + if let Some(target) = type_var_target_in_list( + db, + instance.def_id_value(db), + instance.type_var_elems(db), + file, + offset, + ) { + return Some(target); + } + for function in instance.methods(db) { + if let Some(target) = function_type_var_target_at(db, *function, file, offset) { + return Some(target); + } + } + } + Item::ContractDef(contract) => { + if let Some(target) = type_var_target_in_list( + db, + contract.def_id_value(db), + contract.ty_param_elems(db), + file, + offset, + ) { + return Some(target); + } + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) => { + if let Some(target) = + function_type_var_target_at(db, function, file, offset) + { + return Some(target); + } + } + ContractItem::TypeAlias(alias) => { + if let Some(target) = type_var_target_in_list( + db, + alias.def_id_value(db), + alias.ty_param_elems(db), + file, + offset, + ) { + return Some(target); + } + } + ContractItem::AdtDef(adt) => { + if let Some(target) = type_var_target_in_list( + db, + adt.def_id_value(db), + adt.ty_param_elems(db), + file, + offset, + ) { + return Some(target); + } + } + ContractItem::Error { .. } => {} + } + } + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } + } + + None +} + +fn function_type_var_target_at<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + type_var_target_in_list( + db, + function.def_id_value(db), + &function.sig(db).type_vars, + file, + offset, + ) +} + +fn type_var_target_in_list<'db>( + db: &'db dyn hir_ty::Db, + owner: DefId<'db>, + vars: &[SpannedElem<'db, hir::ast::Ident<'db>>], + file: SourceFile, + offset: u32, +) -> Option> { + vars.iter().enumerate().find_map(|(index, var)| { + span_contains_offset(db, var.span(db), file, offset).then(|| { + ReferenceTarget::Local(LocalBinding::TypeVar(TypeVarId { + owner, + index: index as u32, + name: var.atom().text(db).to_owned(), + })) + }) + }) +} + +fn body_resolution_map<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + mut type_vars: Vec>, + imports: &dyn hir_nameres::ImportedNames<'db>, +) -> hir_nameres::BodyResolutionMap<'db> { + let sig = function.sig(db); + type_vars.extend(hir_nameres::type_var_bindings( + function.def_id_value(db), + &sig.type_vars, + )); + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract, + params: hir_nameres::param_bindings(sig.params.atom()), + type_vars, + }; + hir_nameres::resolve_body_with_imports_and_policy( + db, + root_body, + &context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ) +} + +fn collect_item_resolution_locations<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + item_facts: &hir_nameres::ItemResolutionFacts<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, + target: &ReferenceTarget<'db>, + locations: &mut Vec, +) { + for entry in &item_facts.types { + if let Some(qualifier) = type_ref_module_qualifier(db, entry.ty) { + collect_module_qualifier_locations(world, db, env, qualifier, target, locations); + } + if let Some(span) = type_ref_name_span(db, entry.ty) + && resolution_matches_target(db, span, &entry.resolution, target) + { + push_span_location(world, db, span, locations); + } + } + + for entry in &item_facts.preds { + if let Some(span) = pred_ref_class_span(db, entry.pred) + && resolution_matches_target(db, span, &entry.resolution, target) + { + push_span_location(world, db, span, locations); + } + } +} + +fn collect_body_reference_locations<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + body_map: &hir_nameres::BodyResolutionMap<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, + target: &ReferenceTarget<'db>, + locations: &mut Vec, +) { + for entry in &body_map.exprs { + let expr = entry.body.exprs(db).get(entry.expr); + if let Some(span) = expr_reference_span(db, expr) + && resolution_matches_target(db, span, &entry.resolution, target) + { + push_span_location(world, db, span, locations); + } + } + + for entry in &body_map.pats { + let pat = entry.body.pats(db).get(entry.pat); + if let Some(qualifier) = pat_module_qualifier(pat) { + collect_module_qualifier_locations(world, db, env, qualifier, target, locations); + } + let Some(span) = pat_reference_span(db, pat) else { + continue; + }; + if !resolution_matches_target(db, span, &entry.resolution, target) { + continue; + } + if matches!( + target, + ReferenceTarget::Local(LocalBinding::Pattern { body, pat }) + if *body == entry.body && *pat == entry.pat + ) { + continue; + } + push_span_location(world, db, span, locations); + } + + for entry in &body_map.types { + if let Some(qualifier) = type_ref_module_qualifier(db, entry.ty) { + collect_module_qualifier_locations(world, db, env, qualifier, target, locations); + } + if let Some(span) = type_ref_name_span(db, entry.ty) + && resolution_matches_target(db, span, &entry.resolution, target) + { + push_span_location(world, db, span, locations); + } + } + + for entry in &body_map.preds { + if let Some(span) = pred_ref_class_span(db, entry.pred) + && resolution_matches_target(db, span, &entry.resolution, target) + { + push_span_location(world, db, span, locations); + } + } +} + +fn collect_import_export_reference_locations<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + module: Module<'db>, + module_id: nameres::ModuleId<'db>, + scope: &hir_nameres::ItemScopeFacts<'db>, + target: &ReferenceTarget<'db>, + locations: &mut Vec, +) { + let env = nameres::module_env(db, module_id); + + for item in module.items(db) { + match *item { + Item::Import(import) => { + let Some(ImportSelector::Names(names)) = import.selector(db) else { + continue; + }; + for selected in names { + let selected_targets = import_selected_name_resolutions(db, &env, selected) + .iter() + .filter_map(target_from_resolution) + .collect::>(); + if selected_targets.iter().any(|candidate| candidate == target) { + push_span_location(world, db, selected.name.span(db), locations); + } + for (ctor_target, span) in constructor_selector_targets( + db, + import_selected_type_target(db, &env, selected).as_ref(), + selected.constructors.as_ref(), + ) { + if &ctor_target == target { + push_span_location(world, db, span, locations); + } + } + } + } + Item::Export(export) => { + let (names, reexport_module) = match export.kind(db) { + ExportKind::List(names) => (names.as_slice(), None), + ExportKind::ItemsFrom(path, names) => { + let path_ref = nameres::ModulePathRef { + span: export.span(db), + external: None, + segments: path.clone(), + }; + ( + names.as_slice(), + nameres::resolve_module_path(db, module_id, path_ref).ok(), + ) + } + ExportKind::Module(_) | ExportKind::ModuleAs(_, _) => continue, + }; + for exported in names { + let exported_targets = reexport_module.map_or_else( + || export_name_targets(db, scope, &env, exported), + |module| reexport_name_targets(db, module, exported), + ); + let type_target = reexport_module + .and_then(|module| reexport_name_type_target(db, module, exported)) + .or_else(|| export_name_type_target(db, scope, &env, exported)); + if exported_targets.iter().any(|candidate| candidate == target) { + push_span_location(world, db, exported.name.span(db), locations); + } + for (ctor_target, span) in constructor_selector_targets( + db, + type_target.as_ref(), + exported.constructors.as_ref(), + ) { + if &ctor_target == target { + push_span_location(world, db, span, locations); + } + } + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::ContractDef(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } +} + +fn import_selected_name_resolutions<'db>( + db: &'db dyn hir_ty::Db, + env: &dyn hir_nameres::ImportedNames<'db>, + selected: &SelectedName<'db>, +) -> Vec> { + let local_name = selected + .alias + .as_ref() + .unwrap_or(&selected.name) + .atom() + .text(db); + let mut resolutions = [ + env.imported(db, hir_nameres::Namespace::Term, local_name), + env.imported(db, hir_nameres::Namespace::Type, local_name), + ] + .into_iter() + .flatten() + .collect::>(); + resolutions.dedup(); + resolutions +} + +fn import_selected_type_target<'db>( + db: &'db dyn hir_ty::Db, + env: &dyn hir_nameres::ImportedNames<'db>, + selected: &SelectedName<'db>, +) -> Option> { + let local_name = selected + .alias + .as_ref() + .unwrap_or(&selected.name) + .atom() + .text(db); + env.imported(db, hir_nameres::Namespace::Type, local_name) + .and_then(|resolution| target_from_resolution(&resolution)) +} + +fn export_name_resolutions<'db>( + db: &'db dyn hir_ty::Db, + scope: &hir_nameres::ItemScopeFacts<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, + exported: &ExportedName<'db>, +) -> Vec> { + let name = exported.name.atom().text(db); + let mut resolutions = [ + scope.term_resolution(name), + scope.type_resolution(name), + env.imported(db, hir_nameres::Namespace::Term, name), + env.imported(db, hir_nameres::Namespace::Type, name), + ] + .into_iter() + .flatten() + .collect::>(); + resolutions.dedup(); + resolutions +} + +fn export_name_targets<'db>( + db: &'db dyn hir_ty::Db, + scope: &hir_nameres::ItemScopeFacts<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, + exported: &ExportedName<'db>, +) -> Vec> { + let mut targets = export_name_resolutions(db, scope, env, exported) + .iter() + .filter_map(|resolution| { + target_from_resolution_at_span(db, resolution, exported.name.span(db)) + }) + .collect::>(); + targets.dedup(); + targets +} + +fn export_name_type_target<'db>( + db: &'db dyn hir_ty::Db, + scope: &hir_nameres::ItemScopeFacts<'db>, + env: &dyn hir_nameres::ImportedNames<'db>, + exported: &ExportedName<'db>, +) -> Option> { + let name = exported.name.atom().text(db); + scope + .type_resolution(name) + .or_else(|| env.imported(db, hir_nameres::Namespace::Type, name)) + .and_then(|resolution| target_from_resolution(&resolution)) +} + +fn reexport_name_targets<'db>( + db: &'db dyn hir_ty::Db, + module: nameres::ModuleId<'db>, + exported: &ExportedName<'db>, +) -> Vec> { + let name = exported.name.atom().text(db); + let interface = nameres::public_interface(db, module); + let mut targets = [ + interface.terms.get(name), + interface.types.get(name), + interface.classes.get(name), + ] + .into_iter() + .flatten() + .map(|origin| ReferenceTarget::Def(origin.def_id)) + .collect::>(); + targets.dedup(); + targets +} + +fn reexport_name_type_target<'db>( + db: &'db dyn hir_ty::Db, + module: nameres::ModuleId<'db>, + exported: &ExportedName<'db>, +) -> Option> { + let name = exported.name.atom().text(db); + let interface = nameres::public_interface(db, module); + interface + .types + .get(name) + .map(|origin| ReferenceTarget::Def(origin.def_id)) +} + +fn constructor_selector_target_at<'db>( + db: &'db dyn hir_ty::Db, + type_target: Option<&ReferenceTarget<'db>>, + selector: Option<&ConstructorSelector<'db>>, + file: SourceFile, + offset: u32, +) -> Option> { + let ReferenceTarget::Def(ty) = type_target? else { + return None; + }; + let ConstructorSelector::Named(names) = selector? else { + return None; + }; + let adt = find_adt( + db, + parser::parse_file_to_hir(db, ty.file(db)).module(db), + *ty, + )?; + names.iter().find_map(|name| { + if !span_contains_offset(db, name.span(db), file, offset) { + return None; + } + let selected = name.atom().text(db); + adt.ctors(db) + .iter() + .position(|ctor| ctor.name.atom().text(db) == selected) + .map(|index| ReferenceTarget::Ctor { + ty: *ty, + index: CtorIndex::from_usize(index), + }) + }) +} + +fn constructor_selector_targets<'db>( + db: &'db dyn hir_ty::Db, + type_target: Option<&ReferenceTarget<'db>>, + selector: Option<&ConstructorSelector<'db>>, +) -> Vec<(ReferenceTarget<'db>, Span<'db>)> { + let Some(ReferenceTarget::Def(ty)) = type_target else { + return Vec::new(); + }; + let Some(ConstructorSelector::Named(names)) = selector else { + return Vec::new(); + }; + let Some(adt) = find_adt( + db, + parser::parse_file_to_hir(db, ty.file(db)).module(db), + *ty, + ) else { + return Vec::new(); + }; + names + .iter() + .filter_map(|name| { + let selected = name.atom().text(db); + adt.ctors(db) + .iter() + .position(|ctor| ctor.name.atom().text(db) == selected) + .map(|index| { + ( + ReferenceTarget::Ctor { + ty: *ty, + index: CtorIndex::from_usize(index), + }, + name.span(db), + ) + }) + }) + .collect() +} + +fn target_from_resolution<'db>(resolution: &Resolution<'db>) -> Option> { + match resolution { + Resolution::Def { def, .. } => Some(ReferenceTarget::Def(*def)), + Resolution::Ctor { ty, index } => Some(ReferenceTarget::Ctor { + ty: *ty, + index: *index, + }), + Resolution::Param(param) => Some(ReferenceTarget::Param(*param)), + Resolution::Local(local) => Some(ReferenceTarget::Local(local.clone())), + Resolution::Field(field) => Some(ReferenceTarget::Field(*field)), + Resolution::ClassMethod { class, name } => Some(ReferenceTarget::ClassMethod { + class: *class, + name: name.clone(), + }), + Resolution::Module(module_ref) => Some(ReferenceTarget::Module(module_ref.clone())), + Resolution::DotCtorDeferred | Resolution::Builtin(_) | Resolution::Err => None, + } +} + +fn target_from_resolution_at_span<'db>( + db: &'db dyn hir_ty::Db, + resolution: &Resolution<'db>, + span: Span<'db>, +) -> Option> { + let target = target_from_resolution(resolution)?; + import_alias_target_for_span(db, span, &target) + .or_else(|| exported_module_alias_target_for_resolution(db, resolution, span)) + .or(Some(target)) +} + +fn exported_module_alias_target_for_resolution<'db>( + db: &'db dyn hir_ty::Db, + resolution: &Resolution<'db>, + _span: Span<'db>, +) -> Option> { + let Resolution::Module(module_ref) = resolution else { + return None; + }; + let module_id = nameres::module_id_for_source_file(db, module_ref.owner.file(db))?; + let env = nameres::module_env(db, module_id); + + if let Some((parent_name, alias_name)) = module_ref.name.rsplit_once('.') + && let (Some(parent), Some(target)) = ( + env.surface.modules.get(parent_name), + env.surface.modules.get(&module_ref.name), + ) + && nameres::public_interface(db, *parent) + .module_aliases + .get(alias_name) + == Some(target) + { + return exported_module_alias_target(db, *parent, alias_name); + } + None +} + +fn exported_module_alias_target<'db>( + db: &'db dyn hir_ty::Db, + owner_module: nameres::ModuleId<'db>, + name: &str, +) -> Option> { + let file = db.module_file(owner_module)?; + let module = parser::parse_file_to_hir(db, file).module(db); + module + .items(db) + .iter() + .any(|item| { + matches!( + *item, + Item::Export(export) + if matches!( + export.kind(db), + ExportKind::ModuleAs(_, alias) if alias.atom().text(db) == name + ) + ) + }) + .then(|| ReferenceTarget::ExportedModuleAlias { + owner: module.def_id_value(db), + name: name.to_owned(), + }) +} + +fn import_alias_target_for_span<'db>( + db: &'db dyn hir_ty::Db, + span: Span<'db>, + target: &ReferenceTarget<'db>, +) -> Option> { + let absolute = span.resolve_to_absolute(db); + let text = absolute.file().content(db).as_deref()?; + let occurrence = + text.get(absolute.start().as_u32() as usize..absolute.end().as_u32() as usize)?; + let module_id = nameres::module_id_for_source_file(db, absolute.file())?; + let module = parser::parse_file_to_hir(db, absolute.file()).module(db); + let env = nameres::module_env(db, module_id); + + module.items(db).iter().find_map(|item| { + let Item::Import(import) = *item else { + return None; + }; + let ImportSelector::Names(names) = import.selector(db).as_ref()? else { + return None; + }; + names.iter().find_map(|selected| { + let alias = selected.alias.as_ref()?; + if alias.atom().text(db) != occurrence { + return None; + } + let belongs_to_alias = import_selected_name_resolutions(db, &env, selected) + .iter() + .filter_map(target_from_resolution) + .any(|imported| &imported == target); + if belongs_to_alias { + import_alias_target(db, module, selected) + } else { + None + } + }) + }) +} + +fn import_alias_target<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + selected: &SelectedName<'db>, +) -> Option> { + let alias = selected.alias.as_ref()?; + Some(ReferenceTarget::ImportAlias { + owner: module.def_id_value(db), + name: alias.atom().text(db).to_owned(), + }) +} + +fn resolution_matches_target<'db>( + db: &'db dyn hir_ty::Db, + span: Span<'db>, + resolution: &Resolution<'db>, + target: &ReferenceTarget<'db>, +) -> bool { + target_from_resolution_at_span(db, resolution, span).as_ref() == Some(target) +} + +/// Returns the declaration span for a semantic reference target. +pub fn target_declaration_span<'db>( + db: &'db dyn hir_ty::Db, + target: &ReferenceTarget<'db>, +) -> Option { + match target { + ReferenceTarget::Def(def) => def_name_span(db, *def), + ReferenceTarget::Ctor { ty, index } => ctor_name_span(db, *ty, index.as_usize()), + ReferenceTarget::Param(param) => param_name_span(db, *param), + ReferenceTarget::Local(LocalBinding::Let { body, stmt }) => { + let stmt = body.stmts(db).get(*stmt); + let span = match &stmt.kind { + StmtKind::Let { name, .. } => name.span(db), + _ => stmt.span, + }; + Some(span.resolve_to_absolute(db)) + } + ReferenceTarget::Local(LocalBinding::Pattern { body, pat }) => { + let pat = body.pats(db).get(*pat); + let span = match &pat.kind { + PatKind::Var(name) => name.span(db), + _ => pat.span, + }; + Some(span.resolve_to_absolute(db)) + } + ReferenceTarget::Local(LocalBinding::TypeVar(type_var)) => type_var_name_span(db, type_var), + ReferenceTarget::Field(field) => field_name_span(db, *field), + ReferenceTarget::ClassMethod { class, name } => class_method_name_span(db, *class, name), + ReferenceTarget::Module(module_ref) => module_binding_name_span(db, module_ref), + ReferenceTarget::ImportAlias { owner, name } => import_alias_name_span(db, *owner, name), + ReferenceTarget::ExportedModuleAlias { owner, name } => { + exported_module_alias_name_span(db, *owner, name) + } + } +} + +fn exported_module_alias_name_span<'db>( + db: &'db dyn hir_ty::Db, + owner: DefId<'db>, + name: &str, +) -> Option { + let module = parser::parse_file_to_hir(db, owner.file(db)).module(db); + if module.def_id_value(db) != owner { + return None; + } + module.items(db).iter().find_map(|item| { + let Item::Export(export) = *item else { + return None; + }; + let ExportKind::ModuleAs(_, alias) = export.kind(db) else { + return None; + }; + (alias.atom().text(db) == name).then(|| alias.span(db).resolve_to_absolute(db)) + }) +} + +fn module_binding_name_span<'db>( + db: &'db dyn hir_ty::Db, + module_ref: &ModuleRef<'db>, +) -> Option { + let file = module_ref.owner.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + hir_nameres::item_scope_facts(db, module) + .modules + .iter() + .find(|entry| { + matches!( + &entry.resolution, + Resolution::Module(candidate) if candidate == module_ref + ) + }) + .map(|entry| entry.span.resolve_to_absolute(db)) +} + +fn import_alias_name_span<'db>( + db: &'db dyn hir_ty::Db, + owner: DefId<'db>, + name: &str, +) -> Option { + let file = owner.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + if module.def_id_value(db) != owner { + return None; + } + module.items(db).iter().find_map(|item| { + let Item::Import(import) = *item else { + return None; + }; + let ImportSelector::Names(names) = import.selector(db).as_ref()? else { + return None; + }; + names.iter().find_map(|selected| { + let alias = selected.alias.as_ref()?; + if alias.atom().text(db) != name { + return None; + } + Some(alias.span(db).resolve_to_absolute(db)) + }) + }) +} + +fn def_name_span<'db>(db: &'db dyn hir_ty::Db, def: DefId<'db>) -> Option { + let file = def.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_def_name_span_in_module(db, module, def) + .map(|span| span.resolve_to_absolute(db)) + .or_else(|| { + let location = resolve_def_location(db.def_location_table(file), def)?; + Some(AbsoluteSpan::new( + location.file, + location.base_offset, + location.base_offset, + )) + }) +} + +fn find_def_name_span_in_module<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + Item::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + Item::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + Item::ClassDef(class) if class.def_id_value(db) == def => { + return Some(class.head(db).kind(db).class.span(db)); + } + Item::InstanceDef(instance) if instance.def_id_value(db) == def => { + return Some(instance.head(db).span(db)); + } + Item::ContractDef(contract) => { + if contract.def_id_value(db) == def { + return Some(contract.name_elem(db).span(db)); + } + if let Some(span) = find_def_name_span_in_contract(db, contract, def) { + return Some(span); + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn find_def_name_span_in_contract<'db>( + db: &'db dyn hir_ty::Db, + contract: ContractDef<'db>, + def: DefId<'db>, +) -> Option> { + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == def => { + return Some(function.sig(db).name.span(db)); + } + ContractItem::TypeAlias(alias) if alias.def_id_value(db) == def => { + return Some(alias.name_elem(db).span(db)); + } + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => { + return Some(adt.name_elem(db).span(db)); + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + + None +} + +fn ctor_name_span<'db>( + db: &'db dyn hir_ty::Db, + ty: DefId<'db>, + index: usize, +) -> Option { + let file = ty.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_adt(db, module, ty)? + .ctors(db) + .get(index) + .map(|ctor| ctor.name.span(db).resolve_to_absolute(db)) +} + +fn find_adt<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match *item { + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + _ => None, + }), + _ => None, + }) +} + +fn param_name_span<'db>(db: &'db dyn hir_ty::Db, param: ParamId<'db>) -> Option { + let file = param.body.def_id(db).file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_param_span_in_module(db, module, param.body, param.index.as_usize()) + .map(|span| span.resolve_to_absolute(db)) +} + +fn find_param_span_in_module<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + if let Some(span) = find_param_span_in_function(db, function, body, index) { + return Some(span); + } + } + Item::ContractDef(contract) => { + for contract_item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *contract_item + && let Some(span) = find_param_span_in_function(db, function, body, index) + { + return Some(span); + } + } + } + Item::InstanceDef(instance) => { + for function in instance.methods(db) { + if let Some(span) = find_param_span_in_function(db, *function, body, index) { + return Some(span); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn find_param_span_in_function<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + if function.body(db) == Some(body) { + return function + .sig(db) + .params + .atom() + .get(index) + .and_then(|param| param_name_or_whole_span(db, param)); + } + + find_lambda_param_span(db, function.body(db)?, body, index) +} + +fn find_lambda_param_span<'db>( + db: &'db dyn hir_ty::Db, + root: FuncBody<'db>, + body: FuncBody<'db>, + index: usize, +) -> Option> { + let mut stack = vec![root]; + while let Some(current) = stack.pop() { + for (_, expr) in current.exprs(db).iter() { + if let ExprKind::Lambda { + params, + body: lambda_body, + .. + } = &expr.kind + { + if *lambda_body == body { + return params + .atom() + .get(index) + .and_then(|param| param_name_or_whole_span(db, param)); + } + stack.push(*lambda_body); + } + } + } + + None +} + +fn param_name_or_whole_span<'db>( + db: &'db dyn hir_ty::Db, + param: &FuncParam<'db>, +) -> Option> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => Some(name.span(db)), + FuncParam::Error { span } if !span.resolve_to_absolute(db).is_empty() => Some(*span), + FuncParam::Error { .. } => None, + } +} + +fn type_var_name_span<'db>( + db: &'db dyn hir_ty::Db, + type_var: &TypeVarId<'db>, +) -> Option { + let file = type_var.owner.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_type_var_span_in_module(db, module, type_var.owner, type_var.index as usize) + .map(|span| span.resolve_to_absolute(db)) +} + +fn find_type_var_span_in_module<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + owner: DefId<'db>, + index: usize, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) if function.def_id_value(db) == owner => { + return function + .sig(db) + .type_vars + .get(index) + .map(|var| var.span(db)); + } + Item::TypeAlias(alias) if alias.def_id_value(db) == owner => { + return alias.ty_param_elems(db).get(index).map(|var| var.span(db)); + } + Item::AdtDef(adt) if adt.def_id_value(db) == owner => { + return adt.ty_param_elems(db).get(index).map(|var| var.span(db)); + } + Item::ClassDef(class) if class.def_id_value(db) == owner => { + return class.type_var_elems(db).get(index).map(|var| var.span(db)); + } + Item::InstanceDef(instance) => { + if instance.def_id_value(db) == owner { + return instance + .type_var_elems(db) + .get(index) + .map(|var| var.span(db)); + } + for function in instance.methods(db) { + if function.def_id_value(db) == owner { + return function + .sig(db) + .type_vars + .get(index) + .map(|var| var.span(db)); + } + } + } + Item::ContractDef(contract) => { + if contract.def_id_value(db) == owner { + return contract + .ty_param_elems(db) + .get(index) + .map(|var| var.span(db)); + } + if let Some(span) = find_type_var_span_in_contract(db, contract, owner, index) { + return Some(span); + } + } + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn find_type_var_span_in_contract<'db>( + db: &'db dyn hir_ty::Db, + contract: ContractDef<'db>, + owner: DefId<'db>, + index: usize, +) -> Option> { + for item in contract.items(db) { + match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == owner => { + return function + .sig(db) + .type_vars + .get(index) + .map(|var| var.span(db)); + } + ContractItem::TypeAlias(alias) if alias.def_id_value(db) == owner => { + return alias.ty_param_elems(db).get(index).map(|var| var.span(db)); + } + ContractItem::AdtDef(adt) if adt.def_id_value(db) == owner => { + return adt.ty_param_elems(db).get(index).map(|var| var.span(db)); + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + + None +} + +fn field_name_span<'db>(db: &'db dyn hir_ty::Db, field: FieldId<'db>) -> Option { + let file = field.contract.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_contract(db, module, field.contract)? + .fields(db) + .get(field.index.as_usize()) + .map(|field| field.name().span(db).resolve_to_absolute(db)) +} + +fn class_method_name_span<'db>( + db: &'db dyn hir_ty::Db, + class: DefId<'db>, + name: &str, +) -> Option { + let file = class.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + find_class(db, module, class)? + .methods(db) + .iter() + .find(|method| method.name.atom().text(db) == name) + .map(|method| method.name.span(db).resolve_to_absolute(db)) +} + +fn find_contract<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(contract), + _ => None, + }) +} + +fn find_class<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match *item { + Item::ClassDef(class) if class.def_id_value(db) == def => Some(class), + _ => None, + }) +} + +fn stmt_binding_span<'db>( + db: &'db dyn hir_ty::Db, + body: FuncBody<'db>, + stmt: hir::arena::Id>, +) -> Option> { + let stmt = body.stmts(db).get(stmt); + match &stmt.kind { + StmtKind::Let { name, .. } => Some(name.span(db)), + _ => None, + } +} + +fn expr_reference_span<'db>(db: &'db dyn hir_ty::Db, expr: &Expr<'db>) -> Option> { + match &expr.kind { + ExprKind::Ident(name) => Some(name.span(db)), + ExprKind::DotCtor { name, .. } | ExprKind::Field { field: name, .. } => Some(name.span(db)), + ExprKind::Error => Some(expr.span), + ExprKind::Lit(_) + | ExprKind::Proxy { .. } + | ExprKind::Lambda { .. } + | ExprKind::BinOp { .. } + | ExprKind::Index { .. } + | ExprKind::Call { .. } + | ExprKind::Conversion { .. } + | ExprKind::TypeAscription { .. } + | ExprKind::UnaryOp { .. } + | ExprKind::If { .. } + | ExprKind::Tuple(_) => None, + } +} + +fn pat_reference_span<'db>(db: &'db dyn hir_ty::Db, pat: &Pat<'db>) -> Option> { + match &pat.kind { + PatKind::Var(name) => Some(name.span(db)), + PatKind::Ctor { head, .. } => Some(head.name().span(db)), + PatKind::Error => Some(pat.span), + PatKind::Wildcard + | PatKind::Lit(_) + | PatKind::ComptimeLabel { .. } + | PatKind::Tuple { .. } => None, + } +} + +fn pat_module_qualifier<'a, 'db>( + pat: &'a Pat<'db>, +) -> Option<&'a SpannedElem<'db, hir::ast::Ident<'db>>> { + match &pat.kind { + PatKind::Ctor { + head: PatCtorHead::Qualified { qualifier, .. }, + .. + } => Some(qualifier), + PatKind::Wildcard + | PatKind::Var(_) + | PatKind::Lit(_) + | PatKind::ComptimeLabel { .. } + | PatKind::Ctor { .. } + | PatKind::Tuple { .. } + | PatKind::Error => None, + } +} + +fn type_ref_module_qualifier<'a, 'db>( + db: &'db dyn hir_ty::Db, + ty: TypeRef<'db>, +) -> Option<&'a SpannedElem<'db, hir::ast::Ident<'db>>> +where + 'db: 'a, +{ + match ty.kind(db) { + TypeRefKind::Named { + qualifier: Some(qualifier), + .. + } => Some(qualifier), + TypeRefKind::FixedArray { element, .. } => type_ref_module_qualifier(db, *element), + TypeRefKind::Named { + qualifier: None, .. + } + | TypeRefKind::Fn { .. } + | TypeRefKind::Comptime { .. } + | TypeRefKind::Tuple { .. } + | TypeRefKind::Error { .. } => None, + } +} + +fn module_qualifier_target_at<'db>( + db: &'db dyn hir_ty::Db, + env: &dyn hir_nameres::ImportedNames<'db>, + qualifier: &SpannedElem<'db, hir::ast::Ident<'db>>, + file: SourceFile, + offset: u32, +) -> Option> { + module_qualifier_occurrences(db, env, qualifier) + .into_iter() + .find_map(|(target, span)| span_contains_offset(db, span, file, offset).then_some(target)) +} + +fn collect_module_qualifier_locations<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + env: &dyn hir_nameres::ImportedNames<'db>, + qualifier: &SpannedElem<'db, hir::ast::Ident<'db>>, + target: &ReferenceTarget<'db>, + locations: &mut Vec, +) { + for (candidate, span) in module_qualifier_occurrences(db, env, qualifier) { + if &candidate == target { + push_span_location(world, db, span, locations); + } + } +} + +fn module_qualifier_occurrences<'db>( + db: &'db dyn hir_ty::Db, + env: &dyn hir_nameres::ImportedNames<'db>, + qualifier: &SpannedElem<'db, hir::ast::Ident<'db>>, +) -> Vec<(ReferenceTarget<'db>, Span<'db>)> { + let text = qualifier.atom().text(db); + let qualifier_span = qualifier.span(db); + let base = qualifier_span.begin().as_u32(); + let mut segment_start = 0usize; + let mut occurrences = Vec::new(); + + for prefix_end in text + .match_indices('.') + .map(|(offset, _)| offset) + .chain(std::iter::once(text.len())) + { + let prefix = &text[..prefix_end]; + let Some(Resolution::Module(module_ref)) = + env.imported(db, hir_nameres::Namespace::Module, prefix) + else { + segment_start = prefix_end.saturating_add(1); + continue; + }; + let Some(start) = u32::try_from(segment_start) + .ok() + .and_then(|relative| base.checked_add(relative)) + else { + continue; + }; + let Some(end) = u32::try_from(prefix_end) + .ok() + .and_then(|relative| base.checked_add(relative)) + else { + continue; + }; + let span = Span::new( + qualifier_span.anchor(), + Offset::new(start), + Offset::new(end), + ); + let resolution = Resolution::Module(module_ref); + if let Some(target) = target_from_resolution_at_span(db, &resolution, span) { + occurrences.push((target, span)); + } + segment_start = prefix_end.saturating_add(1); + } + + occurrences +} + +fn type_ref_name_span<'db>(db: &'db dyn hir_ty::Db, ty: TypeRef<'db>) -> Option> { + match ty.kind(db) { + TypeRefKind::Named { name, .. } => Some(name.span(db)), + TypeRefKind::FixedArray { element, .. } => type_ref_name_span(db, *element), + TypeRefKind::Error { span } => Some(*span), + TypeRefKind::Fn { .. } | TypeRefKind::Comptime { .. } | TypeRefKind::Tuple { .. } => None, + } +} + +fn pred_ref_class_span<'db>(db: &'db dyn hir_ty::Db, pred: PredRef<'db>) -> Option> { + Some(pred.kind(db).class.span(db)) +} + +fn span_contains_offset<'db>( + db: &'db dyn hir_ty::Db, + span: Span<'db>, + file: SourceFile, + offset: u32, +) -> bool { + let absolute = span.resolve_to_absolute(db); + absolute.file() == file + && absolute.start().as_u32() <= offset + && offset < absolute.end().as_u32() +} + +fn push_span_location<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + span: Span<'db>, + locations: &mut Vec, +) { + if let Some(location) = location_for_span(world, db, span.resolve_to_absolute(db)) { + locations.push(location); + } +} + +fn location_for_span( + world: &WorldState, + db: &vfs::AnalysisHost, + span: AbsoluteSpan, +) -> Option { + let uri = world.client_uri_for_vfs_url(span.file().url(db).as_str())?; + let range = if let Some(line_index) = world.line_index(&uri) { + line_index.range(span.start().as_u32(), span.end().as_u32()) + } else { + let text = span.file().content(db).as_deref()?; + LineIndexExt::new(text).range(span.start().as_u32(), span.end().as_u32()) + }; + + Some(Location { uri, range }) +} + +fn sort_dedup_locations(locations: &mut Vec) { + locations.sort_by(|left, right| { + left.uri + .as_str() + .cmp(right.uri.as_str()) + .then_with(|| left.range.start.line.cmp(&right.range.start.line)) + .then_with(|| left.range.start.character.cmp(&right.range.start.character)) + .then_with(|| left.range.end.line.cmp(&right.range.end.line)) + .then_with(|| left.range.end.character.cmp(&right.range.end.character)) + }); + locations.dedup_by(|left, right| { + left.uri == right.uri + && left.range.start == right.range.start + && left.range.end == right.range.end + }); +} + +#[cfg(test)] +mod tests { + use lsp_types::Range; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn world_with_main_and_math(main: &str, math: &str) -> (WorldState, Url, Url) { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math.to_owned())); + (world, main_uri, math_uri) + } + + #[test] + fn fixed_array_type_reference_helpers_reach_the_element_name() { + let source = "function use(value: M.Word[4][2]) {}\n"; + let (world, uri) = world_with_main(source); + let db = world.db(); + let path = world.vfs_path_for_uri(&uri).expect("main path"); + let file = db.source_file(&path).expect("main source"); + let module = parser::parse_file_to_hir(db, file).module(db); + let ty = module + .items(db) + .iter() + .find_map(|item| match item { + Item::FunctionDef(function) => { + function + .sig(db) + .params + .atom() + .iter() + .find_map(|param| match param { + FuncParam::Typed { ty, .. } => Some(*ty), + FuncParam::Untyped { .. } | FuncParam::Error { .. } => None, + }) + } + _ => None, + }) + .expect("fixed-array parameter type"); + + let qualifier = type_ref_module_qualifier(db, ty).expect("element qualifier"); + assert_eq!(qualifier.atom().text(db), "M"); + let name_span = type_ref_name_span(db, ty) + .expect("element name span") + .resolve_to_absolute(db); + let expected = source.find("Word").expect("element name") as u32; + assert_eq!(name_span.start().as_u32(), expected); + assert_eq!(name_span.end().as_u32(), expected + "Word".len() as u32); + } + + #[test] + fn parameter_references_include_uses_and_optional_declaration() { + let source = "function id(x: word) returns (word) {\n let y = x;\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let first_use = (source.find("let y = x").expect("first use") + "let y = ".len()) as u32; + let second_use = (source.find("return x").expect("second use") + "return ".len()) as u32; + let declaration = source.find("x: word").expect("declaration") as u32; + let position = line_index.byte_to_position(first_use); + + let references = handle_references(&world, &uri, position, false).expect("references"); + assert_eq!( + ranges_for_uri(&references, &uri), + vec![ + line_index.range(first_use, first_use + 1), + line_index.range(second_use, second_use + 1), + ] + ); + + let references = + handle_references(&world, &uri, position, true).expect("references with declaration"); + assert_eq!( + ranges_for_uri(&references, &uri), + vec![ + line_index.range(declaration, declaration + 1), + line_index.range(first_use, first_use + 1), + line_index.range(second_use, second_use + 1), + ] + ); + } + + #[test] + fn top_level_function_declaration_finds_call_site() { + let source = "\ +function target() returns (word) { + return 1; +} + +function caller() returns (word) { + return target(); +} +"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let declaration = source.find("target").expect("declaration") as u32; + let call = source.rfind("target").expect("call") as u32; + let position = line_index.byte_to_position(declaration); + + let references = handle_references(&world, &uri, position, false).expect("references"); + assert_eq!( + ranges_for_uri(&references, &uri), + vec![line_index.range(call, call + "target".len() as u32)] + ); + } + + #[test] + fn std_references_exclude_the_unopenable_embedded_declaration() { + let source = "import {addWord} from std;\nfunction main() returns (word) { return addWord(1, 2); }\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let import = source.find("addWord").expect("import") as u32; + let call = source.rfind("addWord").expect("call") as u32; + + let references = handle_references(&world, &uri, line_index.byte_to_position(call), true) + .expect("references"); + + assert_eq!( + ranges_for_uri(&references, &uri), + vec![ + line_index.range(import, import + "addWord".len() as u32), + line_index.range(call, call + "addWord".len() as u32), + ] + ); + assert!(references.iter().all(|location| location.uri == uri)); + } + + #[test] + fn import_and_export_names_are_references_to_exported_item() { + let main = + "import {double} from math;\nfunction main() returns (word) { return double(21); }\n"; + let math = + "function double(x: word) returns (word) { return x + x; }\nexport { double };\n"; + let (world, main_uri, math_uri) = world_with_main_and_math(main, math); + let main_index = world.line_index(&main_uri).expect("main line index"); + let math_index = world.line_index(&math_uri).expect("math line index"); + let import = main.find("double").expect("import") as u32; + let call = main.rfind("double").expect("call") as u32; + let declaration = math.find("double").expect("declaration") as u32; + let export = math.rfind("double").expect("export") as u32; + + let references = + handle_references(&world, &main_uri, main_index.byte_to_position(call), true) + .expect("references"); + + assert_eq!( + ranges_for_uri_filtered(&references, &main_uri), + vec![ + main_index.range(import, import + "double".len() as u32), + main_index.range(call, call + "double".len() as u32), + ] + ); + assert_eq!( + ranges_for_uri_filtered(&references, &math_uri), + vec![ + math_index.range(declaration, declaration + "double".len() as u32), + math_index.range(export, export + "double".len() as u32), + ] + ); + } + + #[test] + fn selected_import_alias_references_do_not_rename_the_source_symbol() { + let main = "import {double as twice} from math;\nfunction main() returns (word) { return twice(21); }\n"; + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + let (world, main_uri, math_uri) = world_with_main_and_math(main, math); + let main_index = world.line_index(&main_uri).expect("main line index"); + let math_index = world.line_index(&math_uri).expect("math line index"); + let source_name = main.find("double").expect("selected source name") as u32; + let alias = main.find("twice").expect("alias declaration") as u32; + let use_offset = main.rfind("twice").expect("alias use") as u32; + + let alias_references = handle_references( + &world, + &main_uri, + main_index.byte_to_position(use_offset), + true, + ) + .expect("alias references"); + assert_eq!( + ranges_for_uri(&alias_references, &main_uri), + vec![ + main_index.range(alias, alias + "twice".len() as u32), + main_index.range(use_offset, use_offset + "twice".len() as u32), + ] + ); + + let declaration = math.find("double").expect("definition") as u32; + let export = math.rfind("double").expect("export") as u32; + let source_references = handle_references( + &world, + &math_uri, + math_index.byte_to_position(declaration), + true, + ) + .expect("source references"); + assert_eq!( + ranges_for_uri_filtered(&source_references, &main_uri), + vec![main_index.range(source_name, source_name + "double".len() as u32)] + ); + assert_eq!( + ranges_for_uri_filtered(&source_references, &math_uri), + vec![ + math_index.range(declaration, declaration + "double".len() as u32), + math_index.range(export, export + "double".len() as u32), + ] + ); + } + + #[test] + fn module_alias_references_include_declaration_and_qualifier() { + let main = + "import * as M from math;\nfunction main() returns (word) { return M.value(); }\n"; + let math = "function value() returns (word) { return 1; }\nexport { value };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main line index"); + let declaration = main.find("M from").expect("module alias") as u32; + let qualifier = main.rfind("M.value").expect("module qualifier") as u32; + + let references = + handle_references(&world, &main_uri, index.byte_to_position(qualifier), true) + .expect("module alias references"); + + assert_eq!( + ranges_for_uri(&references, &main_uri), + vec![ + index.range(declaration, declaration + 1), + index.range(qualifier, qualifier + 1), + ] + ); + } + + #[test] + fn module_alias_references_include_type_and_pattern_qualifiers() { + let main = "\ +import * as M from math; +function unwrap(token: M.Token) returns (word) { + match (token) { case M.Token.Ok(value) { return value; } case M.Token.Err(value) { return value; } } +} +"; + let model = "enum Token { Ok(word), Err(word) }\nexport { Token(Ok, Err) };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, model); + let index = world.line_index(&main_uri).expect("main line index"); + let declaration = main.find("M from").expect("module alias") as u32; + let type_qualifier = main.find("M.Token").expect("type qualifier") as u32; + let ok_qualifier = main.find("M.Token.Ok").expect("Ok qualifier") as u32; + let err_qualifier = main.find("M.Token.Err").expect("Err qualifier") as u32; + + let references = handle_references( + &world, + &main_uri, + index.byte_to_position(type_qualifier), + true, + ) + .expect("module alias references"); + + assert_eq!( + ranges_for_uri(&references, &main_uri), + vec![ + index.range(declaration, declaration + 1), + index.range(type_qualifier, type_qualifier + 1), + index.range(ok_qualifier, ok_qualifier + 1), + index.range(err_qualifier, err_qualifier + 1), + ] + ); + } + + #[test] + fn local_reexport_of_selected_alias_is_a_local_reference() { + let main = "\ +import {double as twice} from math; +export { twice }; +function main() returns (word) { return twice(21); } +"; + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main index"); + let declaration = main.find("twice").expect("alias declaration") as u32; + let export = main.find("export { twice").expect("export") as u32 + "export { ".len() as u32; + let use_offset = main.rfind("twice").expect("alias use") as u32; + + let references = + handle_references(&world, &main_uri, index.byte_to_position(use_offset), false) + .expect("alias references without declaration"); + assert_eq!( + ranges_for_uri(&references, &main_uri), + vec![ + index.range(export, export + "twice".len() as u32), + index.range(use_offset, use_offset + "twice".len() as u32), + ] + ); + + let references = + handle_references(&world, &main_uri, index.byte_to_position(use_offset), true) + .expect("alias references with declaration"); + assert_eq!( + ranges_for_uri(&references, &main_uri), + vec![ + index.range(declaration, declaration + "twice".len() as u32), + index.range(export, export + "twice".len() as u32), + index.range(use_offset, use_offset + "twice".len() as u32), + ] + ); + } + + #[test] + fn exported_module_alias_references_include_downstream_qualifiers() { + let mut world = WorldState::new(); + let util_uri = Url::parse("file:///main/util.solc").expect("util uri"); + let facade_uri = Url::parse("file:///main/facade.solc").expect("facade uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let consumer_uri = Url::parse("file:///main/consumer.solc").expect("consumer uri"); + let util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let facade = "export util as Tools;\n"; + let main = "import * as facade from facade;\nfunction main() returns (word) { return facade.Tools.value(); }\n"; + let consumer = "import * as facade from facade;\nfunction consume() returns (word) { return facade.Tools.value(); }\n"; + assert!(world.open_document(util_uri, util.to_owned())); + assert!(world.open_document(facade_uri.clone(), facade.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(consumer_uri.clone(), consumer.to_owned())); + let facade_index = world.line_index(&facade_uri).expect("facade index"); + let main_index = world.line_index(&main_uri).expect("main index"); + let consumer_index = world.line_index(&consumer_uri).expect("consumer index"); + let declaration = facade.find("Tools").expect("export alias") as u32; + let qualifier = main.find("Tools").expect("qualified alias") as u32; + let consumer_qualifier = consumer.find("Tools").expect("consumer qualifier") as u32; + + let references = handle_references( + &world, + &main_uri, + main_index.byte_to_position(qualifier), + true, + ) + .expect("exported module alias references"); + + assert_eq!( + ranges_for_uri_filtered(&references, &facade_uri), + vec![facade_index.range(declaration, declaration + 5)] + ); + assert_eq!( + ranges_for_uri_filtered(&references, &main_uri), + vec![main_index.range(qualifier, qualifier + 5)] + ); + assert_eq!( + ranges_for_uri_filtered(&references, &consumer_uri), + vec![consumer_index.range(consumer_qualifier, consumer_qualifier + 5)] + ); + } + + #[test] + fn ambiguous_term_and_type_selector_has_no_single_reference_target() { + let main = "import {Thing} from math;\nfunction use(x: Thing) returns (word) { return Thing.Thing(); }\n"; + let math = "\ +enum Thing { MakeThing } +function Thing() returns (word) { return 1; } +export { Thing }; +"; + let (world, main_uri, _) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main index"); + let selector = main.find("Thing").expect("selector") as u32; + + assert_eq!( + reference_target_at(&world, &main_uri, index.byte_to_position(selector)), + None + ); + } + + #[test] + fn exported_module_alias_identity_survives_unaliased_reexport() { + let mut world = WorldState::new(); + let util_uri = Url::parse("file:///main/util.solc").expect("util uri"); + let facade_uri = Url::parse("file:///main/facade.solc").expect("facade uri"); + let bridge_uri = Url::parse("file:///main/bridge.solc").expect("bridge uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let facade = "export util as Tools;\n"; + let bridge = "export facade;\n"; + let main = "import * as bridge from bridge;\nfunction main() returns (word) { return bridge.facade.Tools.value(); }\n"; + assert!(world.open_document(util_uri, util.to_owned())); + assert!(world.open_document(facade_uri.clone(), facade.to_owned())); + assert!(world.open_document(bridge_uri.clone(), bridge.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let facade_index = world.line_index(&facade_uri).expect("facade index"); + let main_index = world.line_index(&main_uri).expect("main index"); + let declaration = facade.find("Tools").expect("alias declaration") as u32; + let use_offset = main.find("Tools").expect("downstream use") as u32; + + let references = handle_references( + &world, + &main_uri, + main_index.byte_to_position(use_offset), + true, + ) + .expect("transitive module alias references"); + assert_eq!( + ranges_for_uri_filtered(&references, &facade_uri), + vec![facade_index.range(declaration, declaration + 5)] + ); + assert!(ranges_for_uri_filtered(&references, &bridge_uri).is_empty()); + assert_eq!( + ranges_for_uri_filtered(&references, &main_uri), + vec![main_index.range(use_offset, use_offset + 5)] + ); + } + + #[test] + fn constructor_selectors_and_reexports_are_references() { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let bridge_uri = Url::parse("file:///main/bridge.solc").expect("bridge uri"); + let model_uri = Url::parse("file:///main/model.solc").expect("model uri"); + let main = "import {Token} from bridge;\nfunction make(x: word) returns (Token) { return Token.Ok(x); }\n"; + let bridge = "export model.{Token(Ok)};\n"; + let model = "enum Token { Ok(word), Err(word) }\nexport { Token(Ok, Err) };\n"; + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(bridge_uri.clone(), bridge.to_owned())); + assert!(world.open_document(model_uri.clone(), model.to_owned())); + let main_index = world.line_index(&main_uri).expect("main index"); + let bridge_index = world.line_index(&bridge_uri).expect("bridge index"); + let model_index = world.line_index(&model_uri).expect("model index"); + let use_ctor = main.rfind("Ok").expect("constructor use") as u32; + let reexport_ctor = bridge.find("Ok").expect("re-export constructor") as u32; + let declaration = model.find("Ok").expect("constructor declaration") as u32; + let export_ctor = model.rfind("Ok").expect("export constructor") as u32; + let position = main_index.byte_to_position(use_ctor); + assert!( + reference_target_at(&world, &main_uri, position).is_some(), + "expected constructor target; diagnostics: {:#?}", + crate::diagnostics::compute_diagnostics(&world, &main_uri) + ); + + let references = + handle_references(&world, &main_uri, position, true).expect("constructor references"); + + assert_eq!( + ranges_for_uri_filtered(&references, &main_uri), + vec![main_index.range(use_ctor, use_ctor + 2)] + ); + assert_eq!( + ranges_for_uri_filtered(&references, &bridge_uri), + vec![bridge_index.range(reexport_ctor, reexport_ctor + 2)] + ); + assert_eq!( + ranges_for_uri_filtered(&references, &model_uri), + vec![ + model_index.range(declaration, declaration + 2), + model_index.range(export_ctor, export_ctor + 2), + ] + ); + } + + fn ranges_for_uri(locations: &[Location], uri: &Url) -> Vec { + assert!( + locations.iter().all(|location| location.uri == *uri), + "expected all locations in {uri}, got {locations:#?}" + ); + locations.iter().map(|location| location.range).collect() + } + + fn ranges_for_uri_filtered(locations: &[Location], uri: &Url) -> Vec { + locations + .iter() + .filter(|location| location.uri == *uri) + .map(|location| location.range) + .collect() + } +} diff --git a/crates/lsp/src/rename.rs b/crates/lsp/src/rename.rs new file mode 100644 index 00000000..27730bc6 --- /dev/null +++ b/crates/lsp/src/rename.rs @@ -0,0 +1,700 @@ +//! Rename support over the wasm-clean LSP core. + +use std::{ + cmp::Ordering, + collections::{BTreeMap, HashMap}, +}; + +use lsp_types::{Location, Position, PrepareRenameResponse, Range, TextEdit, Url, WorkspaceEdit}; + +use crate::{ + LineIndexExt, + references::{collect_reference_locations, reference_target_at, target_supports_text_rename}, + state::WorldState, +}; + +/// Computes the rename range for the user symbol at a source position. +pub fn handle_prepare_rename( + world: &WorldState, + uri: &Url, + position: Position, +) -> Option { + let target = reference_target_at(world, uri, position)?; + if !target_supports_text_rename(world, &target) { + return None; + } + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let locations = editable_reference_locations(world, &target)?; + + locations + .into_iter() + .filter(|location| location.uri == *uri) + .find_map(|location| { + location_contains_offset(line_index, &location, offset).then_some(location.range) + }) + .map(PrepareRenameResponse::Range) +} + +/// Computes a workspace edit that renames the user symbol at a source position. +pub fn handle_rename( + world: &WorldState, + uri: &Url, + position: Position, + new_name: &str, +) -> Option { + if !parser::is_valid_identifier(new_name) { + return None; + } + + let target = reference_target_at(world, uri, position)?; + if !target_supports_text_rename(world, &target) { + return None; + } + let locations = editable_reference_locations(world, &target)?; + let changes = text_edits_by_uri(locations, new_name)?; + + Some(WorkspaceEdit { + changes: Some(changes), + document_changes: None, + change_annotations: None, + }) +} + +fn editable_reference_locations( + world: &WorldState, + target: &crate::references::ReferenceTarget, +) -> Option> { + let locations = collect_reference_locations(world, target, true); + (!locations.is_empty() + && locations + .iter() + .all(|location| world.line_index(&location.uri).is_some())) + .then_some(locations) +} + +fn text_edits_by_uri( + locations: Vec, + new_name: &str, +) -> Option>> { + let mut locations_by_uri = BTreeMap::)>::new(); + for location in locations { + locations_by_uri + .entry(location.uri.as_str().to_owned()) + .or_insert_with(|| (location.uri.clone(), Vec::new())) + .1 + .push(location.range); + } + + let mut changes = HashMap::new(); + for (_, (uri, mut ranges)) in locations_by_uri { + ranges.sort_by(compare_ranges); + ranges.dedup_by(|left, right| left.start == right.start && left.end == right.end); + if ranges_overlap(&ranges) { + return None; + } + + let edits = ranges + .into_iter() + .map(|range| TextEdit { + range, + new_text: new_name.to_owned(), + }) + .collect(); + changes.insert(uri, edits); + } + + Some(changes) +} + +fn location_contains_offset(line_index: &LineIndexExt, location: &Location, offset: u32) -> bool { + let Some(start) = line_index.position_to_byte(location.range.start) else { + return false; + }; + let Some(end) = line_index.position_to_byte(location.range.end) else { + return false; + }; + + start <= offset && offset < end +} + +fn ranges_overlap(ranges: &[Range]) -> bool { + ranges + .windows(2) + .any(|pair| compare_positions(&pair[1].start, &pair[0].end).is_lt()) +} + +fn compare_ranges(left: &Range, right: &Range) -> Ordering { + compare_positions(&left.start, &right.start) + .then_with(|| compare_positions(&left.end, &right.end)) +} + +fn compare_positions(left: &Position, right: &Position) -> Ordering { + left.line + .cmp(&right.line) + .then_with(|| left.character.cmp(&right.character)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn world_with_main_and_math(main: &str, math: &str) -> (WorldState, Url, Url) { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(math_uri.clone(), math.to_owned())); + (world, main_uri, math_uri) + } + + #[test] + fn renaming_parameter_edits_declaration_and_uses() { + let source = "function id(x: word) returns (word) {\n let y = x;\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let declaration = source.find("x: word").expect("declaration") as u32; + let first_use = (source.find("let y = x").expect("first use") + "let y = ".len()) as u32; + let second_use = (source.find("return x").expect("second use") + "return ".len()) as u32; + let position = line_index.byte_to_position(first_use); + + let edit = handle_rename(&world, &uri, position, "renamed").expect("rename edit"); + + assert_eq!(edit.document_changes, None); + assert_eq!(edit.change_annotations, None); + let changes = edit.changes.expect("changes"); + let edits = changes.get(&uri).expect("current file edits"); + assert_eq!(edits.len(), 3); + assert!(edits.iter().all(|edit| edit.new_text == "renamed")); + assert_eq!( + edits.iter().map(|edit| edit.range).collect::>(), + vec![ + line_index.range(declaration, declaration + 1), + line_index.range(first_use, first_use + 1), + line_index.range(second_use, second_use + 1), + ] + ); + } + + #[test] + fn prepare_rename_returns_user_symbol_range_but_not_builtin_or_keyword() { + let source = "function id(x: word) returns (word) {\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let use_offset = (source.find("return x").expect("use") + "return ".len()) as u32; + + let prepare = handle_prepare_rename(&world, &uri, line_index.byte_to_position(use_offset)) + .expect("prepare rename"); + match prepare { + PrepareRenameResponse::Range(range) => { + assert_eq!(range, line_index.range(use_offset, use_offset + 1)); + } + other => panic!("expected range prepare response, got {other:?}"), + } + + let builtin = source.find("word").expect("builtin") as u32; + assert_eq!( + handle_prepare_rename(&world, &uri, line_index.byte_to_position(builtin)), + None + ); + let keyword = source.find("return").expect("keyword") as u32; + assert_eq!( + handle_prepare_rename(&world, &uri, line_index.byte_to_position(keyword)), + None + ); + } + + #[test] + fn rename_rejects_invalid_new_name() { + let source = "function id(x: word) returns (word) {\n return x;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let use_offset = (source.find("return x").expect("use") + "return ".len()) as u32; + let position = line_index.byte_to_position(use_offset); + + assert!(handle_rename(&world, &uri, position, "1bad").is_none()); + assert!(handle_rename(&world, &uri, position, "").is_none()); + assert!(handle_rename(&world, &uri, position, "return").is_none()); + assert!(handle_rename(&world, &uri, position, "bad-name").is_none()); + assert!(handle_rename(&world, &uri, position, "_bad").is_none()); + assert!(handle_rename(&world, &uri, position, "λvalue").is_some()); + } + + #[test] + fn renaming_exported_function_edits_import_and_export_names() { + let main = + "import {double} from math;\nfunction main() returns (word) { return double(21); }\n"; + let math = + "function double(x: word) returns (word) { return x + x; }\nexport { double };\n"; + let (world, main_uri, math_uri) = world_with_main_and_math(main, math); + let main_index = world.line_index(&main_uri).expect("main line index"); + let math_index = world.line_index(&math_uri).expect("math line index"); + let import = main.find("double").expect("import") as u32; + let call = main.rfind("double").expect("call") as u32; + let declaration = math.find("double").expect("declaration") as u32; + let export = math.rfind("double").expect("export") as u32; + + let edit = handle_rename( + &world, + &main_uri, + main_index.byte_to_position(call), + "twice", + ) + .expect("rename edit"); + let changes = edit.changes.expect("changes"); + + let main_edits = changes.get(&main_uri).expect("main edits"); + assert!(main_edits.iter().all(|edit| edit.new_text == "twice")); + assert_eq!( + main_edits.iter().map(|edit| edit.range).collect::>(), + vec![ + main_index.range(import, import + "double".len() as u32), + main_index.range(call, call + "double".len() as u32), + ] + ); + + let math_edits = changes.get(&math_uri).expect("math edits"); + assert!(math_edits.iter().all(|edit| edit.new_text == "twice")); + assert_eq!( + math_edits.iter().map(|edit| edit.range).collect::>(), + vec![ + math_index.range(declaration, declaration + "double".len() as u32), + math_index.range(export, export + "double".len() as u32), + ] + ); + } + + #[test] + fn embedded_std_symbol_is_not_offered_for_rename() { + let source = "import {addWord} from std;\nfunction main() returns (word) { return addWord(1, 2); }\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let call = source.rfind("addWord").expect("call") as u32; + let position = line_index.byte_to_position(call); + + assert_eq!(handle_prepare_rename(&world, &uri, position), None); + assert_eq!(handle_rename(&world, &uri, position, "sumWords"), None); + } + + #[test] + fn renaming_exported_function_from_defining_module_edits_importer() { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main = + "import {double} from math;\nfunction main() returns (word) { return double(21); }\n"; + let math = "\ +function double(x: word) returns (word) { return x + x; } +function local() returns (word) { return double(2); } +export { double }; +"; + assert!(world.open_document(math_uri.clone(), math.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let main_index = world.line_index(&main_uri).expect("main line index"); + let math_index = world.line_index(&math_uri).expect("math line index"); + let import = main.find("double").expect("import") as u32; + let call = main.rfind("double").expect("call") as u32; + let declaration = math.find("double").expect("declaration") as u32; + let local_call = math.find("double(2)").expect("local call") as u32; + let export = math.rfind("double").expect("export") as u32; + + let edit = handle_rename( + &world, + &math_uri, + math_index.byte_to_position(local_call), + "twice", + ) + .expect("rename edit"); + let changes = edit.changes.expect("changes"); + + let main_edits = changes.get(&main_uri).expect("main edits"); + assert!(main_edits.iter().all(|edit| edit.new_text == "twice")); + assert_eq!( + main_edits.iter().map(|edit| edit.range).collect::>(), + vec![ + main_index.range(import, import + "double".len() as u32), + main_index.range(call, call + "double".len() as u32), + ] + ); + + let math_edits = changes.get(&math_uri).expect("math edits"); + assert!(math_edits.iter().all(|edit| edit.new_text == "twice")); + assert_eq!( + math_edits.iter().map(|edit| edit.range).collect::>(), + vec![ + math_index.range(declaration, declaration + "double".len() as u32), + math_index.range(local_call, local_call + "double".len() as u32), + math_index.range(export, export + "double".len() as u32), + ] + ); + } + + #[test] + fn renaming_selected_import_alias_only_edits_local_alias_uses() { + let main = "import {double as twice} from math;\nfunction main() returns (word) { return twice(21); }\n"; + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + let (world, main_uri, math_uri) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main index"); + let alias = main.find("twice").expect("alias declaration") as u32; + let use_offset = main.rfind("twice").expect("alias use") as u32; + + let edit = handle_rename( + &world, + &main_uri, + index.byte_to_position(use_offset), + "applyTwice", + ) + .expect("alias rename"); + let changes = edit.changes.expect("changes"); + assert!(!changes.contains_key(&math_uri)); + assert_eq!( + changes + .get(&main_uri) + .expect("main edits") + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![ + index.range(alias, alias + "twice".len() as u32), + index.range(use_offset, use_offset + "twice".len() as u32), + ] + ); + } + + #[test] + fn renaming_explicit_module_alias_edits_alias_and_qualifiers() { + let main = + "import * as M from math;\nfunction main() returns (word) { return M.value(); }\n"; + let math = "function value() returns (word) { return 1; }\nexport { value };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main index"); + let alias = main.find("M from").expect("alias declaration") as u32; + let use_offset = main.rfind("M.value").expect("alias use") as u32; + + let edit = handle_rename( + &world, + &main_uri, + index.byte_to_position(use_offset), + "Math", + ) + .expect("module alias rename"); + let edits = edit + .changes + .expect("changes") + .remove(&main_uri) + .expect("main edits"); + assert_eq!( + edits.iter().map(|edit| edit.range).collect::>(), + vec![ + index.range(alias, alias + 1), + index.range(use_offset, use_offset + 1), + ] + ); + } + + #[test] + fn renaming_module_alias_updates_type_and_pattern_qualifiers() { + let main = "\ +import * as M from math; +function unwrap(token: M.Token) returns (word) { + match (token) { case M.Token.Ok(value) { return value; } case M.Token.Err(value) { return value; } } +} +"; + let model = "enum Token { Ok(word), Err(word) }\nexport { Token(Ok, Err) };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, model); + let index = world.line_index(&main_uri).expect("main index"); + let declaration = main.find("M from").expect("alias declaration") as u32; + let type_qualifier = main.find("M.Token").expect("type qualifier") as u32; + let ok_qualifier = main.find("M.Token.Ok").expect("Ok qualifier") as u32; + let err_qualifier = main.find("M.Token.Err").expect("Err qualifier") as u32; + + let edit = handle_rename( + &world, + &main_uri, + index.byte_to_position(type_qualifier), + "Model", + ) + .expect("module alias rename"); + let edits = edit + .changes + .expect("changes") + .remove(&main_uri) + .expect("main edits"); + assert_eq!( + edits.iter().map(|edit| edit.range).collect::>(), + vec![ + index.range(declaration, declaration + 1), + index.range(type_qualifier, type_qualifier + 1), + index.range(ok_qualifier, ok_qualifier + 1), + index.range(err_qualifier, err_qualifier + 1), + ] + ); + } + + #[test] + fn exported_selected_alias_is_not_offered_an_incomplete_text_rename() { + let main = "\ +import {double as twice} from math; +export { twice }; +function main() returns (word) { return twice(21); } +"; + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + let (world, main_uri, _) = world_with_main_and_math(main, math); + let index = world.line_index(&main_uri).expect("main index"); + let use_offset = main.rfind("twice").expect("alias use") as u32; + let position = index.byte_to_position(use_offset); + + assert_eq!(handle_prepare_rename(&world, &main_uri, position), None); + assert_eq!(handle_rename(&world, &main_uri, position, "thrice"), None); + } + + #[test] + fn renaming_exported_module_alias_updates_downstream_qualifiers() { + let mut world = WorldState::new(); + let util_uri = Url::parse("file:///main/util.solc").expect("util uri"); + let facade_uri = Url::parse("file:///main/facade.solc").expect("facade uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let consumer_uri = Url::parse("file:///main/consumer.solc").expect("consumer uri"); + let util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let facade = "export util as Tools;\n"; + let main = "import * as facade from facade;\nfunction main() returns (word) { return facade.Tools.value(); }\n"; + let consumer = "import * as facade from facade;\nfunction consume() returns (word) { return facade.Tools.value(); }\n"; + assert!(world.open_document(util_uri, util.to_owned())); + assert!(world.open_document(facade_uri.clone(), facade.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(consumer_uri.clone(), consumer.to_owned())); + let facade_index = world.line_index(&facade_uri).expect("facade index"); + let main_index = world.line_index(&main_uri).expect("main index"); + let consumer_index = world.line_index(&consumer_uri).expect("consumer index"); + let declaration = facade.find("Tools").expect("export alias") as u32; + let qualifier = main.find("Tools").expect("qualified alias") as u32; + let consumer_qualifier = consumer.find("Tools").expect("consumer qualifier") as u32; + + let edit = handle_rename( + &world, + &main_uri, + main_index.byte_to_position(qualifier), + "Helpers", + ) + .expect("exported module alias rename"); + let changes = edit.changes.expect("changes"); + assert_eq!( + changes[&facade_uri] + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![facade_index.range(declaration, declaration + 5)] + ); + assert_eq!( + changes[&main_uri] + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![main_index.range(qualifier, qualifier + 5)] + ); + assert_eq!( + changes[&consumer_uri] + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![consumer_index.range(consumer_qualifier, consumer_qualifier + 5)] + ); + assert!( + changes[&consumer_uri] + .iter() + .all(|edit| edit.new_text == "Helpers") + ); + } + + #[test] + fn source_definition_rename_is_rejected_across_exported_selected_alias() { + let mut world = WorldState::new(); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let bridge_uri = Url::parse("file:///main/bridge.solc").expect("bridge uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let math = "function double(x: word) returns (word) { return x; }\nexport { double };\n"; + let bridge = "import {double as twice} from math;\nexport { twice };\n"; + let main = + "import {twice} from bridge;\nfunction main() returns (word) { return twice(1); }\n"; + assert!(world.open_document(math_uri.clone(), math.to_owned())); + assert!(world.open_document(bridge_uri, bridge.to_owned())); + assert!(world.open_document(main_uri, main.to_owned())); + let index = world.line_index(&math_uri).expect("math index"); + let declaration = math.find("double").expect("source declaration") as u32; + let position = index.byte_to_position(declaration); + + assert_eq!(handle_prepare_rename(&world, &math_uri, position), None); + assert_eq!(handle_rename(&world, &math_uri, position, "timesTwo"), None); + } + + #[test] + fn multi_root_rename_never_edits_same_spelling_in_another_root() { + let base = std::env::temp_dir().join("solcore-lsp-rename-multi-root"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root"); + let right_root = Url::from_directory_path(&right_path).expect("right root"); + let left_main = Url::from_file_path(left_path.join("main.solc")).expect("left main"); + let left_math = Url::from_file_path(left_path.join("math.solc")).expect("left math"); + let right_main = Url::from_file_path(right_path.join("main.solc")).expect("right main"); + let right_math = Url::from_file_path(right_path.join("math.solc")).expect("right math"); + let left_source = + "import {value} from lib.math;\nfunction left() returns (word) { return value(); }\n"; + let right_source = + "import {value} from lib.math;\nfunction right() returns (word) { return value(); }\n"; + let left_library = "function value() returns (word) { return 1; }\nexport { value };\n"; + let right_library = "function value() returns (word) { return 2; }\nexport { value };\n"; + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root, + vec![ + (left_main.clone(), left_source.to_owned()), + (left_math.clone(), left_library.to_owned()), + ], + ), + ( + right_root, + vec![ + (right_main.clone(), right_source.to_owned()), + (right_math.clone(), right_library.to_owned()), + ], + ), + ]); + assert!(world.open_document(left_main.clone(), left_source.to_owned())); + let index = world.line_index(&left_main).expect("left index"); + let use_offset = left_source.rfind("value").expect("left use") as u32; + + let edit = handle_rename( + &world, + &left_main, + index.byte_to_position(use_offset), + "renamed", + ) + .expect("left rename"); + let changes = edit.changes.expect("changes"); + assert_eq!(changes.len(), 2); + assert!(changes.contains_key(&left_main)); + assert!(changes.contains_key(&left_math)); + assert!(!changes.contains_key(&right_main)); + assert!(!changes.contains_key(&right_math)); + } + + #[test] + fn renaming_exported_module_alias_updates_unaliased_reexport_chain() { + let mut world = WorldState::new(); + let util_uri = Url::parse("file:///main/util.solc").expect("util uri"); + let facade_uri = Url::parse("file:///main/facade.solc").expect("facade uri"); + let bridge_uri = Url::parse("file:///main/bridge.solc").expect("bridge uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let facade = "export util as Tools;\n"; + let bridge = "export facade;\n"; + let main = "import * as bridge from bridge;\nfunction main() returns (word) { return bridge.facade.Tools.value(); }\n"; + assert!(world.open_document(util_uri, util.to_owned())); + assert!(world.open_document(facade_uri.clone(), facade.to_owned())); + assert!(world.open_document(bridge_uri.clone(), bridge.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let facade_index = world.line_index(&facade_uri).expect("facade index"); + let main_index = world.line_index(&main_uri).expect("main index"); + let declaration = facade.find("Tools").expect("alias declaration") as u32; + let use_offset = main.find("Tools").expect("downstream use") as u32; + + let edit = handle_rename( + &world, + &main_uri, + main_index.byte_to_position(use_offset), + "Helpers", + ) + .expect("transitive module alias rename"); + let changes = edit.changes.expect("changes"); + assert_eq!( + changes[&facade_uri] + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![facade_index.range(declaration, declaration + 5)] + ); + assert!(!changes.contains_key(&bridge_uri)); + assert_eq!( + changes[&main_uri] + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![main_index.range(use_offset, use_offset + 5)] + ); + } + + #[test] + fn default_module_reexport_without_alias_is_not_text_renameable() { + let mut world = WorldState::new(); + let util_uri = Url::parse("file:///main/util.solc").expect("util uri"); + let facade_uri = Url::parse("file:///main/facade.solc").expect("facade uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let facade = "export util;\n"; + let main = "import * as facade from facade;\nfunction main() returns (word) { return facade.util.value(); }\n"; + assert!(world.open_document(util_uri, util.to_owned())); + assert!(world.open_document(facade_uri, facade.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let index = world.line_index(&main_uri).expect("main index"); + let module = main.find("util").expect("default module alias") as u32; + let position = index.byte_to_position(module); + + assert_eq!(handle_prepare_rename(&world, &main_uri, position), None); + assert_eq!(handle_rename(&world, &main_uri, position, "tools"), None); + } + + #[test] + fn renaming_constructor_updates_import_and_export_selectors() { + let mut world = WorldState::new(); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let model_uri = Url::parse("file:///main/model.solc").expect("model uri"); + let main = "import {Token} from model;\nfunction make(x: word) returns (Token) { return Token.Ok(x); }\n"; + let model = "enum Token { Ok(word), Err(word) }\nexport { Token(Ok, Err) };\n"; + assert!(world.open_document(main_uri.clone(), main.to_owned())); + assert!(world.open_document(model_uri.clone(), model.to_owned())); + let main_index = world.line_index(&main_uri).expect("main index"); + let model_index = world.line_index(&model_uri).expect("model index"); + let use_ctor = main.rfind("Ok").expect("constructor use") as u32; + let declaration = model.find("Ok").expect("constructor declaration") as u32; + let export_ctor = model.rfind("Ok").expect("export constructor") as u32; + + let edit = handle_rename( + &world, + &main_uri, + main_index.byte_to_position(use_ctor), + "Success", + ) + .expect("constructor rename"); + let changes = edit.changes.expect("changes"); + assert_eq!( + changes + .get(&main_uri) + .expect("main edits") + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![main_index.range(use_ctor, use_ctor + 2)] + ); + assert_eq!( + changes + .get(&model_uri) + .expect("model edits") + .iter() + .map(|edit| edit.range) + .collect::>(), + vec![ + model_index.range(declaration, declaration + 2), + model_index.range(export_ctor, export_ctor + 2), + ] + ); + } +} diff --git a/crates/lsp/src/resolve.rs b/crates/lsp/src/resolve.rs new file mode 100644 index 00000000..2f02bcd9 --- /dev/null +++ b/crates/lsp/src/resolve.rs @@ -0,0 +1,171 @@ +//! Shared core helpers for source-position semantic lookup. + +use std::path::Path; + +use hir::{ + anchor::DefId, + arena::Id, + ast::{ + function::{Expr, ExprKind, FuncBody}, + item::{ContractItem, FunctionDef, Item, Module}, + }, + input::SourceFile, + nameres::{self as hir_nameres, TypeVarBinding}, +}; +use lsp_types::Url; +use nameres::Db as _; + +use crate::state::WorldState; + +/// Resolves a client document URI to its logical module in the analysis graph. +/// +/// LSP requests must use the module that owns the requested document rather +/// than the workspace's compilation entry. Import visibility is module-local, +/// and the entry may be an unrelated document that happened to open first. +pub(crate) fn module_id_for_uri<'db>( + world: &WorldState, + db: &'db vfs::AnalysisHost, + uri: &Url, +) -> Option> { + let path = world.vfs_path_for_uri(uri)?; + let tree = db.module_tree(); + let key = nameres::module_key_for_path( + nameres::LibraryId::Main, + tree.main_root(db), + Path::new(&path), + )?; + let module = nameres::module_id_from_key(db, &key); + db.module_file(module)?; + Some(module) +} + +/// A function-like body that owns a requested source offset. +pub(crate) struct FunctionAtOffset<'db> { + pub(crate) function: FunctionDef<'db>, + pub(crate) root_body: FuncBody<'db>, + pub(crate) enclosing_contract: Option>, + pub(crate) inherited_type_vars: Vec>, +} + +/// Returns the smallest expression whose absolute range contains `offset`. +pub(crate) fn innermost_expr<'db>( + db: &'db dyn hir_ty::Db, + root_body: FuncBody<'db>, + file: SourceFile, + offset: u32, +) -> Option<(FuncBody<'db>, Id>)> { + let mut best = None; + let mut stack = vec![root_body]; + + while let Some(body) = stack.pop() { + for (expr_id, expr) in body.exprs(db).iter() { + let absolute = expr.span.resolve_to_absolute(db); + if absolute.file() == file + && absolute.start().as_u32() <= offset + && offset < absolute.end().as_u32() + { + let width = absolute.len(); + if best + .as_ref() + .is_none_or(|(_, _, best_width)| width < *best_width) + { + best = Some((body, expr_id, width)); + } + } + + if let ExprKind::Lambda { + body: lambda_body, .. + } = &expr.kind + { + stack.push(*lambda_body); + } + } + } + + best.map(|(body, expr, _)| (body, expr)) +} + +/// Returns the function/method whose body contains an expression at `offset`. +pub(crate) fn function_owning_offset<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + file: SourceFile, + offset: u32, +) -> Option> { + for item in module.items(db) { + match *item { + Item::FunctionDef(function) => { + if let Some(found) = + function_contains_offset(db, function, None, Vec::new(), file, offset) + { + return Some(found); + } + } + Item::ContractDef(contract) => { + let inherited = hir_nameres::type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + ); + for contract_item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *contract_item + && let Some(found) = function_contains_offset( + db, + function, + Some(contract.def_id_value(db)), + inherited.clone(), + file, + offset, + ) + { + return Some(found); + } + } + } + Item::InstanceDef(instance) => { + let inherited = hir_nameres::type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + ); + for function in instance.methods(db) { + if let Some(found) = function_contains_offset( + db, + *function, + None, + inherited.clone(), + file, + offset, + ) { + return Some(found); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + None +} + +fn function_contains_offset<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: Vec>, + file: SourceFile, + offset: u32, +) -> Option> { + let root_body = function.body(db)?; + innermost_expr(db, root_body, file, offset)?; + Some(FunctionAtOffset { + function, + root_body, + enclosing_contract, + inherited_type_vars, + }) +} diff --git a/crates/lsp/src/selection_range.rs b/crates/lsp/src/selection_range.rs new file mode 100644 index 00000000..1fb530da --- /dev/null +++ b/crates/lsp/src/selection_range.rs @@ -0,0 +1,527 @@ +//! Hierarchical smart-selection ranges. +//! +//! Each requested position receives an innermost token followed by containing +//! lexical delimiters, HIR syntax nodes, its source line, enclosing items, and +//! finally the full module. The lexical layers keep selection useful while the +//! parser is recovering from incomplete edits. + +use std::cmp::Reverse; + +use hir::{ + ast::{ + function::FuncBody, + item::{ContractItem, FunctionDef, Item}, + }, + span::{Span, Spanned}, +}; +use lsp_types::{Position, SelectionRange, Url}; + +use crate::{ + folding::{ByteRange, scan_source}, + state::WorldState, +}; + +/// Computes one containment chain for every requested position, preserving +/// request order. +/// +/// The LSP requires a result for each input position, so an invalid UTF-16 +/// position invalidates the request and returns `None` rather than returning a +/// shorter, misaligned result array. +pub fn handle_selection_range( + world: &WorldState, + uri: &Url, + positions: &[Position], +) -> Option> { + let line_index = world.line_index(uri)?; + let source = line_index.text(); + let source_len = u32::try_from(source.len()).ok()?; + let lexical = scan_source(source); + let hir_ranges = hir_selection_ranges(world, uri, source.len()); + let mut result = Vec::with_capacity(positions.len()); + + for position in positions { + let offset = line_index.position_to_byte(*position)? as usize; + let mut candidates = Vec::new(); + if let Some(leaf) = leaf_range_at(source, offset) { + candidates.push(leaf); + } + candidates.extend( + lexical + .delimiters + .iter() + .map(|delimiter| delimiter.range) + .filter(|range| range.contains_offset(offset)), + ); + candidates.extend( + lexical + .block_comments + .iter() + .copied() + .filter(|range| range.contains_offset(offset)), + ); + candidates.extend( + hir_ranges + .iter() + .copied() + .filter(|range| range.contains_offset(offset)), + ); + let line = line_range(source, offset); + if candidates + .iter() + .all(|candidate| line.contains_range(*candidate) || candidate.contains_range(line)) + { + candidates.push(line); + } + candidates.push(ByteRange { + start: 0, + end: source.len(), + }); + + candidates.sort_by_key(|range| (range.len(), Reverse(range.start), range.end)); + candidates.dedup(); + + let mut chain = Vec::::new(); + for candidate in candidates { + if chain + .last() + .is_none_or(|current| candidate.contains_range(*current)) + { + chain.push(candidate); + } + } + + let mut parent = None; + for range in chain.into_iter().rev() { + let start = u32::try_from(range.start).ok()?; + let end = u32::try_from(range.end).ok()?; + debug_assert!(end <= source_len); + parent = Some(SelectionRange { + range: line_index.range(start, end), + parent: parent.map(Box::new), + }); + } + result.push(parent.expect("the module range is always present")); + } + + Some(result) +} + +fn line_range(source: &str, offset: usize) -> ByteRange { + let start = source[..offset] + .rfind('\n') + .map_or(0, |newline| newline + 1); + let mut end = source[offset..] + .find('\n') + .map_or(source.len(), |newline| offset + newline); + if end > start && source.as_bytes().get(end - 1) == Some(&b'\r') { + end -= 1; + } + ByteRange { start, end } +} + +fn leaf_range_at(source: &str, offset: usize) -> Option { + let bytes = source.as_bytes(); + let mut i = 0usize; + let mut previous = None; + + while i < bytes.len() { + let start = i; + let end = match bytes[i] { + byte if byte.is_ascii_whitespace() => { + i += 1; + continue; + } + b'/' if bytes.get(i + 1) == Some(&b'/') => { + i += 2; + while i < bytes.len() && bytes[i] != b'\n' { + i += 1; + } + i + } + b'/' if bytes.get(i + 1) == Some(&b'*') => { + let mut depth = 1usize; + i += 2; + while i < bytes.len() && depth > 0 { + if bytes.get(i..i + 2) == Some(b"/*") { + depth += 1; + i += 2; + } else if bytes.get(i..i + 2) == Some(b"*/") { + depth -= 1; + i += 2; + } else { + i += 1; + } + } + i + } + b'"' => { + i += 1; + while i < bytes.len() { + match bytes[i] { + b'\\' => i = (i + 2).min(bytes.len()), + b'"' => { + i += 1; + break; + } + _ => i += 1, + } + } + i + } + byte if byte.is_ascii_digit() => { + if bytes.get(i..i + 2) == Some(b"0x") + && bytes.get(i + 2).is_some_and(u8::is_ascii_hexdigit) + { + i += 2; + while i < bytes.len() && bytes[i].is_ascii_hexdigit() { + i += 1; + } + } else { + i += 1; + while i < bytes.len() && bytes[i].is_ascii_digit() { + i += 1; + } + } + i + } + _ => { + let first = source[i..] + .chars() + .next() + .expect("valid character boundary"); + if first.is_alphabetic() { + i += first.len_utf8(); + while i < bytes.len() { + let character = source[i..] + .chars() + .next() + .expect("valid character boundary"); + if character.is_alphanumeric() || character == '_' { + i += character.len_utf8(); + continue; + } + if character == '-' + && source[i + 1..] + .chars() + .next() + .is_some_and(char::is_alphabetic) + { + // The lexer permits hyphens only between identifier + // segments (not in ordinary subtraction such as + // `value-1`). + i += 1; + continue; + } + break; + } + } else if is_two_byte_operator(bytes.get(i..i + 2)) { + i += 2; + } else { + i += first.len_utf8(); + } + i + } + }; + + let range = ByteRange { start, end }; + if start <= offset && offset < end { + return Some(range); + } + if end == offset { + previous = Some(range); + } + if start > offset { + break; + } + } + + previous.filter(|_| { + offset == source.len() + || source[offset..] + .chars() + .next() + .is_some_and(char::is_whitespace) + }) +} + +fn is_two_byte_operator(bytes: Option<&[u8]>) -> bool { + matches!( + bytes, + Some( + b":=" + | b"=>" + | b"==" + | b"!=" + | b">=" + | b"<=" + | b"&&" + | b"||" + | b"+=" + | b"-=" + | b"^=" + | b"&=" + | b"|=" + | b"%=" + ) + ) +} + +fn hir_selection_ranges(world: &WorldState, uri: &Url, source_len: usize) -> Vec { + let db = world.db(); + let Some(path) = world.vfs_path_for_uri(uri) else { + return Vec::new(); + }; + let Some(file) = db.source_file(&path) else { + return Vec::new(); + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let mut ranges = Vec::new(); + + for item in module.items(db) { + push_span(db, item.span(db), source_len, &mut ranges); + match item { + Item::FunctionDef(function) => { + collect_function_ranges(db, *function, source_len, &mut ranges); + } + Item::ClassDef(class) => { + for method in class.methods(db) { + push_span(db, method.span(db), source_len, &mut ranges); + } + } + Item::InstanceDef(instance) => { + for method in instance.methods(db) { + collect_function_ranges(db, *method, source_len, &mut ranges); + } + } + Item::ContractDef(contract) => { + for field in contract.fields(db) { + push_span(db, field.span(db), source_len, &mut ranges); + if let Some(init) = field.init() { + push_span(db, init.span(db), source_len, &mut ranges); + for (_, expr) in init.exprs.iter() { + push_span(db, expr.span, source_len, &mut ranges); + } + } + } + for item in contract.items(db) { + push_span(db, item.span(db), source_len, &mut ranges); + if let ContractItem::FunctionDef(function) = item { + collect_function_ranges(db, *function, source_len, &mut ranges); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + ranges +} + +fn collect_function_ranges<'db>( + db: &'db dyn parser::Db, + function: FunctionDef<'db>, + source_len: usize, + ranges: &mut Vec, +) { + push_span(db, function.sig(db).span(db), source_len, ranges); + let Some(body) = function.body(db) else { + return; + }; + collect_body_ranges(db, body, source_len, ranges); +} + +fn collect_body_ranges<'db>( + db: &'db dyn parser::Db, + body: FuncBody<'db>, + source_len: usize, + ranges: &mut Vec, +) { + push_span(db, body.span(db), source_len, ranges); + for (_, stmt) in body.stmts(db).iter() { + push_span(db, stmt.span, source_len, ranges); + } + for (_, expr) in body.exprs(db).iter() { + push_span(db, expr.span, source_len, ranges); + } + for (_, pat) in body.pats(db).iter() { + push_span(db, pat.span, source_len, ranges); + } +} + +fn push_span<'db>( + db: &'db dyn parser::Db, + span: Span<'db>, + source_len: usize, + ranges: &mut Vec, +) { + let absolute = span.resolve_to_absolute(db); + let range = ByteRange { + start: absolute.start().as_u32() as usize, + end: absolute.end().as_u32() as usize, + }; + if range.start < range.end && range.end <= source_len { + ranges.push(range); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn chain(selection: &SelectionRange) -> Vec { + let mut ranges = Vec::new(); + let mut current = Some(selection); + while let Some(selection) = current { + ranges.push(selection.range); + current = selection.parent.as_deref(); + } + ranges + } + + #[test] + fn builds_unicode_safe_leaf_to_module_chain() { + let source = "function main(value: word) returns (word) {\n let café = (value + 1);\n return café;\n}\n"; + let (world, uri) = world_with_main(source); + let line_index = world.line_index(&uri).expect("line index"); + let leaf_start = source.find("café").expect("unicode identifier"); + let position = line_index.byte_to_position((leaf_start + "caf".len()) as u32); + + let selections = + handle_selection_range(&world, &uri, &[position]).expect("selection range"); + let ranges = chain(&selections[0]); + + assert_eq!( + ranges[0], + line_index.range(leaf_start as u32, (leaf_start + "café".len()) as u32) + ); + assert_eq!( + *ranges.last().expect("module range"), + line_index.range(0, source.len() as u32) + ); + for pair in ranges.windows(2) { + let inner_start = line_index.position_to_byte(pair[0].start).unwrap(); + let inner_end = line_index.position_to_byte(pair[0].end).unwrap(); + let outer_start = line_index.position_to_byte(pair[1].start).unwrap(); + let outer_end = line_index.position_to_byte(pair[1].end).unwrap(); + assert!(outer_start <= inner_start && inner_end <= outer_end); + } + assert!( + ranges.len() >= 4, + "expected leaf, syntax/line, item, module" + ); + } + + #[test] + fn preserves_position_order_and_supports_incomplete_syntax() { + let source = "function main() {\n let x = (1 + 2);\n { x; }\n"; + let (world, uri) = world_with_main(source); + let index = world.line_index(&uri).unwrap(); + let one = index.byte_to_position(source.find('1').unwrap() as u32); + let x = index.byte_to_position(source.rfind('x').unwrap() as u32); + + let selections = handle_selection_range(&world, &uri, &[one, x]).unwrap(); + assert_eq!(selections.len(), 2); + assert_eq!( + selections[0].range, + index.range( + source.find('1').unwrap() as u32, + source.find('1').unwrap() as u32 + 1 + ) + ); + assert_eq!( + selections[1].range, + index.range( + source.rfind('x').unwrap() as u32, + source.rfind('x').unwrap() as u32 + 1 + ) + ); + } + + #[test] + fn overlapping_source_line_does_not_hide_multiline_call_selection() { + let source = "\ +function main() returns (word) { + let x = add( + 1, + 2); // trailing + return x; +} +"; + let (world, uri) = world_with_main(source); + let position = Position::new(3, 4); + let ranges = handle_selection_range(&world, &uri, &[position]).expect("selection ranges"); + let mut chain = Vec::new(); + let mut current = Some(&ranges[0]); + while let Some(selection) = current { + chain.push(selection.range); + current = selection.parent.as_deref(); + } + + assert!(chain.contains(&lsp_types::Range::new( + Position::new(1, 13), + Position::new(3, 6) + ))); + } + + #[test] + fn leaf_ranges_follow_identifier_and_operator_token_boundaries() { + let source = "pragma solcore noBoundVariableCondition;\nfunction main() returns (word) {\n let value = 1;\n return value-1;\n}\n"; + let (world, uri) = world_with_main(source); + let index = world.line_index(&uri).unwrap(); + let pragma = source.find("noBoundVariableCondition").unwrap(); + let value = source.rfind("value-1").unwrap(); + let positions = [ + index.byte_to_position((pragma + 3) as u32), + index.byte_to_position((value + 2) as u32), + ]; + + let selections = handle_selection_range(&world, &uri, &positions).unwrap(); + assert_eq!( + selections[0].range, + index.range( + pragma as u32, + (pragma + "noBoundVariableCondition".len()) as u32 + ) + ); + assert_eq!( + selections[1].range, + index.range(value as u32, (value + "value".len()) as u32) + ); + } + + #[test] + fn rejects_out_of_range_and_mid_surrogate_positions() { + let source = "// 😀\n"; + let (world, uri) = world_with_main(source); + + assert_eq!( + handle_selection_range(&world, &uri, &[Position::new(99, 0)]), + None + ); + assert_eq!( + handle_selection_range(&world, &uri, &[Position::new(0, 4)]), + None + ); + } + + #[test] + fn empty_position_list_and_unknown_documents_are_handled() { + let (world, uri) = world_with_main(""); + assert_eq!(handle_selection_range(&world, &uri, &[]), Some(Vec::new())); + + let missing = Url::parse("file:///main/missing.solc").expect("uri"); + assert_eq!(handle_selection_range(&world, &missing, &[]), None); + } +} diff --git a/crates/lsp/src/semantic_tokens.rs b/crates/lsp/src/semantic_tokens.rs new file mode 100644 index 00000000..ecf46d93 --- /dev/null +++ b/crates/lsp/src/semantic_tokens.rs @@ -0,0 +1,763 @@ +//! Semantic token support over the wasm-clean LSP core. + +use hir::{ + anchor::DefId, + ast::{ + function::{ExprKind, FuncBody, FuncParam, FuncSig, PatKind, StmtKind}, + item::{ContractItem, FunctionDef, Item, Module}, + ty::{PredRef, TypeRef, TypeRefKind}, + }, + input::SourceFile, + nameres::{ + self as hir_nameres, BuiltinKind, DefResolutionKind, LocalBinding, Resolution, + TypeVarBinding, + }, + span::{Span, Spanned}, +}; +use lsp_types::{ + SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, SemanticTokensResult, + Url, +}; + +use crate::{resolve::module_id_for_uri, state::WorldState}; + +/// Semantic token types advertised by the server and used by the encoder. +pub const TOKEN_TYPES: &[SemanticTokenType] = &[ + SemanticTokenType::KEYWORD, + SemanticTokenType::FUNCTION, + SemanticTokenType::TYPE, + SemanticTokenType::VARIABLE, + SemanticTokenType::PARAMETER, + SemanticTokenType::PROPERTY, + SemanticTokenType::ENUM_MEMBER, + SemanticTokenType::NAMESPACE, + SemanticTokenType::NUMBER, + SemanticTokenType::STRING, + SemanticTokenType::OPERATOR, + SemanticTokenType::COMMENT, +]; + +/// Semantic token modifiers advertised by the server and used by the encoder. +pub const TOKEN_MODIFIERS: &[SemanticTokenModifier] = &[ + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::READONLY, +]; + +/// Computes full-document semantic tokens for one open source document. +pub fn handle_semantic_tokens_full(world: &WorldState, uri: &Url) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + let scope = hir_nameres::item_scope_facts(db, module); + let item_facts = hir_nameres::resolve_item_type_facts_with_imports(db, module, &scope, &env); + + let mut collector = TokenCollector::new(db, file); + collect_declaration_tokens(db, module, &mut collector); + collect_item_type_tokens(db, &item_facts, &mut collector); + collect_body_tokens(db, module, &env, &mut collector); + + // NOTE(codex): This first pass emits HIR-derived name tokens only. Keywords, + // literals, operators, and comments remain covered by client syntax + // highlighting until the LSP core has a parser-token stream to reuse. + Some(SemanticTokensResult::Tokens(SemanticTokens { + result_id: None, + data: encode_tokens(line_index, collector.finish()), + })) +} + +#[derive(Clone, Copy)] +enum TokenKind { + Function, + Type, + Variable, + Parameter, + Property, + EnumMember, + Namespace, +} + +#[derive(Clone, Copy)] +struct RawToken { + start: u32, + end: u32, + token_type: u32, + token_modifiers_bitset: u32, +} + +struct TokenCollector<'db> { + db: &'db dyn hir_ty::Db, + file: SourceFile, + tokens: Vec, +} + +impl<'db> TokenCollector<'db> { + fn new(db: &'db dyn hir_ty::Db, file: SourceFile) -> Self { + Self { + db, + file, + tokens: Vec::new(), + } + } + + fn add_span(&mut self, span: Span<'db>, kind: TokenKind, modifiers: u32) { + let absolute = span.resolve_to_absolute(self.db); + if absolute.file() != self.file || absolute.is_empty() { + return; + } + + self.tokens.push(RawToken { + start: absolute.start().as_u32(), + end: absolute.end().as_u32(), + token_type: token_type_index(kind), + token_modifiers_bitset: modifiers, + }); + } + + fn finish(self) -> Vec { + self.tokens + } +} + +fn collect_declaration_tokens<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + collector: &mut TokenCollector<'db>, +) { + for item in module.items(db) { + collect_item_declarations(db, *item, collector); + } +} + +fn collect_item_declarations<'db>( + db: &'db dyn hir_ty::Db, + item: Item<'db>, + collector: &mut TokenCollector<'db>, +) { + match item { + Item::FunctionDef(function) => collect_function_declarations(db, function, collector), + Item::TypeAlias(alias) => { + collector.add_span( + alias.name_elem(db).span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, alias.ty_param_elems(db), collector); + } + Item::AdtDef(adt) => { + collector.add_span( + adt.name_elem(db).span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, adt.ty_param_elems(db), collector); + for ctor in adt.ctors(db) { + collector.add_span( + ctor.name.span(db), + TokenKind::EnumMember, + declaration_bitset(), + ); + } + } + Item::ClassDef(class) => { + collector.add_span( + class.head(db).kind(db).class.span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, class.type_var_elems(db), collector); + for method in class.methods(db) { + collect_signature_declarations(db, method, collector); + } + } + Item::InstanceDef(instance) => { + collect_type_var_declarations(db, instance.type_var_elems(db), collector); + for method in instance.methods(db) { + collect_function_declarations(db, *method, collector); + } + } + Item::ContractDef(contract) => { + collector.add_span( + contract.name_elem(db).span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, contract.ty_param_elems(db), collector); + for field in contract.fields(db) { + collector.add_span( + field.name().span(db), + TokenKind::Property, + declaration_bitset(), + ); + } + for item in contract.items(db) { + collect_contract_item_declarations(db, *item, collector); + } + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } +} + +fn collect_contract_item_declarations<'db>( + db: &'db dyn hir_ty::Db, + item: ContractItem<'db>, + collector: &mut TokenCollector<'db>, +) { + match item { + ContractItem::FunctionDef(function) => { + collect_function_declarations(db, function, collector); + } + ContractItem::TypeAlias(alias) => { + collector.add_span( + alias.name_elem(db).span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, alias.ty_param_elems(db), collector); + } + ContractItem::AdtDef(adt) => { + collector.add_span( + adt.name_elem(db).span(db), + TokenKind::Type, + declaration_bitset(), + ); + collect_type_var_declarations(db, adt.ty_param_elems(db), collector); + for ctor in adt.ctors(db) { + collector.add_span( + ctor.name.span(db), + TokenKind::EnumMember, + declaration_bitset(), + ); + } + } + ContractItem::Error { .. } => {} + } +} + +fn collect_function_declarations<'db>( + db: &'db dyn hir_ty::Db, + function: FunctionDef<'db>, + collector: &mut TokenCollector<'db>, +) { + collect_signature_declarations(db, function.sig(db), collector); +} + +fn collect_signature_declarations<'db>( + db: &'db dyn hir_ty::Db, + sig: &FuncSig<'db>, + collector: &mut TokenCollector<'db>, +) { + collector.add_span(sig.name.span(db), TokenKind::Function, declaration_bitset()); + collect_type_var_declarations(db, &sig.type_vars, collector); + collect_param_declarations(db, sig.params.atom(), collector); +} + +fn collect_type_var_declarations<'db>( + db: &'db dyn hir_ty::Db, + vars: &[hir::span::SpannedElem<'db, hir::ast::Ident<'db>>], + collector: &mut TokenCollector<'db>, +) { + for var in vars { + collector.add_span(var.span(db), TokenKind::Type, declaration_bitset()); + } +} + +fn collect_param_declarations<'db>( + db: &'db dyn hir_ty::Db, + params: &[FuncParam<'db>], + collector: &mut TokenCollector<'db>, +) { + for param in params { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + collector.add_span(name.span(db), TokenKind::Parameter, declaration_bitset()); + } + FuncParam::Error { .. } => {} + } + } +} + +fn collect_item_type_tokens<'db>( + db: &'db dyn hir_ty::Db, + facts: &hir_nameres::ItemResolutionFacts<'db>, + collector: &mut TokenCollector<'db>, +) { + for resolved in &facts.types { + collect_type_ref_token(db, resolved.ty, &resolved.resolution, collector); + } + for resolved in &facts.preds { + collect_pred_ref_token(db, resolved.pred, &resolved.resolution, collector); + } +} + +fn collect_body_tokens<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + imports: &dyn hir_nameres::ImportedNames<'db>, + collector: &mut TokenCollector<'db>, +) { + for item in module.items(db) { + collect_item_body_tokens(db, module, *item, None, &[], imports, collector); + } +} + +fn collect_item_body_tokens<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + item: Item<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[TypeVarBinding<'db>], + imports: &dyn hir_nameres::ImportedNames<'db>, + collector: &mut TokenCollector<'db>, +) { + match item { + Item::FunctionDef(function) => collect_function_body_tokens( + db, + module, + function, + enclosing_contract, + inherited_type_vars, + imports, + collector, + ), + Item::InstanceDef(instance) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(hir_nameres::type_var_bindings( + instance.def_id_value(db), + instance.type_var_elems(db), + )); + for method in instance.methods(db) { + collect_function_body_tokens( + db, + module, + *method, + enclosing_contract, + &inherited, + imports, + collector, + ); + } + } + Item::ContractDef(contract) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(hir_nameres::type_var_bindings( + contract.def_id_value(db), + contract.ty_param_elems(db), + )); + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item { + collect_function_body_tokens( + db, + module, + function, + Some(contract.def_id_value(db)), + &inherited, + imports, + collector, + ); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +fn collect_function_body_tokens<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[TypeVarBinding<'db>], + imports: &dyn hir_nameres::ImportedNames<'db>, + collector: &mut TokenCollector<'db>, +) { + let Some(body) = function.body(db) else { + return; + }; + + let sig = function.sig(db); + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(hir_nameres::type_var_bindings( + function.def_id_value(db), + &sig.type_vars, + )); + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract, + params: hir_nameres::param_bindings(sig.params.atom()), + type_vars, + }; + let body_map = hir_nameres::resolve_body_with_imports_and_policy( + db, + body, + &context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ); + + collect_body_map_tokens(db, &body_map, collector); + collect_lambda_param_declarations(db, body, collector); +} + +fn collect_body_map_tokens<'db>( + db: &'db dyn hir_ty::Db, + body_map: &hir_nameres::BodyResolutionMap<'db>, + collector: &mut TokenCollector<'db>, +) { + for entry in &body_map.exprs { + let expr = entry.body.exprs(db).get(entry.expr); + if let Some(span) = expr_token_span(db, expr) + && let Some(kind) = token_kind_for_resolution(&entry.resolution) + { + collector.add_span(span, kind, 0); + } + } + + for entry in &body_map.stmt_bindings { + let stmt = entry.body.stmts(db).get(entry.stmt); + if let StmtKind::Let { name, .. } = &stmt.kind { + collector.add_span(name.span(db), TokenKind::Variable, declaration_bitset()); + } + } + + for entry in &body_map.pats { + let pat = entry.body.pats(db).get(entry.pat); + let Some(span) = pat_token_span(db, pat) else { + continue; + }; + match (&pat.kind, &entry.resolution) { + (PatKind::Var(_), Resolution::Local(LocalBinding::Pattern { .. })) => { + collector.add_span(span, TokenKind::Variable, declaration_bitset()); + } + (PatKind::Ctor { .. }, Resolution::Local(LocalBinding::Pattern { .. })) => { + collector.add_span(span, TokenKind::Variable, declaration_bitset()); + } + _ => { + if let Some(kind) = token_kind_for_resolution(&entry.resolution) { + collector.add_span(span, kind, 0); + } + } + } + } + + for entry in &body_map.types { + collect_type_ref_token(db, entry.ty, &entry.resolution, collector); + } + for entry in &body_map.preds { + collect_pred_ref_token(db, entry.pred, &entry.resolution, collector); + } +} + +fn collect_lambda_param_declarations<'db>( + db: &'db dyn hir_ty::Db, + body: FuncBody<'db>, + collector: &mut TokenCollector<'db>, +) { + for (_, expr) in body.exprs(db).iter() { + if let ExprKind::Lambda { + params, + body: lambda_body, + .. + } = &expr.kind + { + collect_param_declarations(db, params.atom(), collector); + collect_lambda_param_declarations(db, *lambda_body, collector); + } + } +} + +fn collect_type_ref_token<'db>( + db: &'db dyn hir_ty::Db, + ty: TypeRef<'db>, + resolution: &Resolution<'db>, + collector: &mut TokenCollector<'db>, +) { + let Some(kind) = token_kind_for_resolution(resolution) else { + return; + }; + if matches!( + kind, + TokenKind::Type | TokenKind::Namespace | TokenKind::EnumMember + ) && let TypeRefKind::Named { name, .. } = ty.kind(db) + { + collector.add_span(name.span(db), kind, 0); + } +} + +fn collect_pred_ref_token<'db>( + db: &'db dyn hir_ty::Db, + pred: PredRef<'db>, + resolution: &Resolution<'db>, + collector: &mut TokenCollector<'db>, +) { + if let Some(kind @ TokenKind::Type) = token_kind_for_resolution(resolution) { + collector.add_span(pred.kind(db).class.span(db), kind, 0); + } +} + +fn expr_token_span<'db>( + db: &'db dyn hir_ty::Db, + expr: &hir::ast::function::Expr<'db>, +) -> Option> { + match &expr.kind { + ExprKind::Ident(name) => Some(name.span(db)), + ExprKind::DotCtor { name, .. } => Some(name.span(db)), + ExprKind::Field { field, .. } => Some(field.span(db)), + ExprKind::Lit(_) + | ExprKind::Proxy { .. } + | ExprKind::Lambda { .. } + | ExprKind::BinOp { .. } + | ExprKind::Index { .. } + | ExprKind::Call { .. } + | ExprKind::Conversion { .. } + | ExprKind::TypeAscription { .. } + | ExprKind::UnaryOp { .. } + | ExprKind::If { .. } + | ExprKind::Tuple(_) + | ExprKind::Error => None, + } +} + +fn pat_token_span<'db>( + db: &'db dyn hir_ty::Db, + pat: &hir::ast::function::Pat<'db>, +) -> Option> { + match &pat.kind { + PatKind::Var(name) => Some(name.span(db)), + PatKind::Ctor { head, .. } => Some(head.name().span(db)), + PatKind::Wildcard + | PatKind::Lit(_) + | PatKind::ComptimeLabel { .. } + | PatKind::Tuple { .. } + | PatKind::Error => None, + } +} + +fn token_kind_for_resolution(resolution: &Resolution<'_>) -> Option { + match resolution { + Resolution::Def { kind, .. } => match kind { + DefResolutionKind::Function => Some(TokenKind::Function), + DefResolutionKind::Contract + | DefResolutionKind::Adt + | DefResolutionKind::TypeAlias + | DefResolutionKind::ValueType + | DefResolutionKind::Class + | DefResolutionKind::Instance => Some(TokenKind::Type), + }, + Resolution::Local(LocalBinding::Let { .. } | LocalBinding::Pattern { .. }) => { + Some(TokenKind::Variable) + } + Resolution::Local(LocalBinding::TypeVar(_)) => Some(TokenKind::Type), + Resolution::Param(_) => Some(TokenKind::Parameter), + Resolution::Field(_) => Some(TokenKind::Property), + Resolution::Ctor { .. } | Resolution::DotCtorDeferred => Some(TokenKind::EnumMember), + Resolution::ClassMethod { .. } => Some(TokenKind::Function), + Resolution::Module(_) => Some(TokenKind::Namespace), + Resolution::Builtin(kind) => match kind { + BuiltinKind::Type(_) | BuiltinKind::Class(_) => Some(TokenKind::Type), + BuiltinKind::Constructor(_) => Some(TokenKind::EnumMember), + BuiltinKind::Function(_) | BuiltinKind::ClassMethod(_) => Some(TokenKind::Function), + }, + Resolution::Err => None, + } +} + +#[derive(Clone, Copy)] +struct PositionedToken { + line: u32, + start: u32, + length: u32, + token_type: u32, + token_modifiers_bitset: u32, +} + +fn encode_tokens( + line_index: &crate::LineIndexExt, + raw_tokens: Vec, +) -> Vec { + let mut positioned = raw_tokens + .into_iter() + .filter_map(|token| { + let start = line_index.byte_to_position(token.start); + let end = line_index.byte_to_position(token.end); + if start.line != end.line || start.character >= end.character { + return None; + } + Some(PositionedToken { + line: start.line, + start: start.character, + length: end.character - start.character, + token_type: token.token_type, + token_modifiers_bitset: token.token_modifiers_bitset, + }) + }) + .collect::>(); + + positioned.sort_by_key(|token| (token.line, token.start, token.length, token.token_type)); + + let mut filtered = Vec::new(); + let mut previous_end = None::<(u32, u32)>; + for token in positioned { + let overlaps_previous = + previous_end.is_some_and(|(line, end)| token.line == line && token.start < end); + if overlaps_previous { + continue; + } + previous_end = Some((token.line, token.start + token.length)); + filtered.push(token); + } + + let mut data = Vec::with_capacity(filtered.len()); + let mut previous_line = 0; + let mut previous_start = 0; + for token in filtered { + let delta_line = token.line - previous_line; + let delta_start = if delta_line == 0 { + token.start - previous_start + } else { + token.start + }; + data.push(SemanticToken { + delta_line, + delta_start, + length: token.length, + token_type: token.token_type, + token_modifiers_bitset: token.token_modifiers_bitset, + }); + previous_line = token.line; + previous_start = token.start; + } + + data +} + +fn token_type_index(kind: TokenKind) -> u32 { + let token_type = match kind { + TokenKind::Function => &SemanticTokenType::FUNCTION, + TokenKind::Type => &SemanticTokenType::TYPE, + TokenKind::Variable => &SemanticTokenType::VARIABLE, + TokenKind::Parameter => &SemanticTokenType::PARAMETER, + TokenKind::Property => &SemanticTokenType::PROPERTY, + TokenKind::EnumMember => &SemanticTokenType::ENUM_MEMBER, + TokenKind::Namespace => &SemanticTokenType::NAMESPACE, + }; + + TOKEN_TYPES + .iter() + .position(|candidate| candidate == token_type) + .expect("semantic token type must be present in TOKEN_TYPES") as u32 +} + +fn declaration_bitset() -> u32 { + modifier_bitset(&SemanticTokenModifier::DECLARATION) +} + +fn modifier_bitset(modifier: &SemanticTokenModifier) -> u32 { + let index = TOKEN_MODIFIERS + .iter() + .position(|candidate| candidate == modifier) + .expect("semantic token modifier must be present in TOKEN_MODIFIERS"); + 1_u32 << index +} + +#[cfg(test)] +mod tests { + use lsp_types::{SemanticToken, SemanticTokensResult}; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn semantic_tokens_are_non_empty_ordered_and_start_at_first_named_entity() { + let source = "function main(x: word) returns (word) {\n let y = x;\n return y;\n}\n"; + let (world, uri) = world_with_main(source); + + let result = handle_semantic_tokens_full(&world, &uri).expect("semantic tokens"); + let SemanticTokensResult::Tokens(tokens) = result else { + panic!("expected full semantic tokens"); + }; + assert!(!tokens.data.is_empty(), "expected at least one token"); + + let first = tokens.data[0]; + assert_eq!(first.delta_line, 0); + assert_eq!(first.delta_start, source.find("main").expect("main") as u32); + assert_eq!(first.length, "main".len() as u32); + assert_eq!(first.token_type, token_type_index(TokenKind::Function)); + assert_eq!(first.token_modifiers_bitset, declaration_bitset()); + + assert_strictly_ordered(&tokens.data); + } + + #[test] + fn emitted_token_type_indexes_are_covered_by_the_legend() { + let source = "\ +enum Maybe { None, Some(word) } + +contract Box { + value: word; + function get(x: word) returns (word) { + let current = value; + return current + x; + } +} +"; + let (world, uri) = world_with_main(source); + + let result = handle_semantic_tokens_full(&world, &uri).expect("semantic tokens"); + let SemanticTokensResult::Tokens(tokens) = result else { + panic!("expected full semantic tokens"); + }; + assert!( + tokens + .data + .iter() + .all(|token| token.token_type < TOKEN_TYPES.len() as u32), + "emitted token type outside legend: {:?}", + tokens.data + ); + assert!( + tokens + .data + .iter() + .all(|token| token.token_modifiers_bitset < (1_u32 << TOKEN_MODIFIERS.len())), + "emitted token modifier outside legend: {:?}", + tokens.data + ); + } + + fn assert_strictly_ordered(tokens: &[SemanticToken]) { + let mut line = 0; + let mut start = 0; + let mut previous = None::<(u32, u32)>; + for token in tokens { + line += token.delta_line; + if token.delta_line == 0 { + start += token.delta_start; + } else { + start = token.delta_start; + } + if let Some((previous_line, previous_end)) = previous { + assert!( + line > previous_line || (line == previous_line && start >= previous_end), + "tokens are not strictly ordered: {tokens:?}" + ); + } + previous = Some((line, start + token.length)); + } + } +} diff --git a/crates/lsp/src/signature_help.rs b/crates/lsp/src/signature_help.rs new file mode 100644 index 00000000..8109dd6d --- /dev/null +++ b/crates/lsp/src/signature_help.rs @@ -0,0 +1,722 @@ +//! Signature help support over the wasm-clean LSP core. + +use hir::{ + anchor::DefId, + arena::Id, + ast::{ + function::{Expr, ExprKind, FuncBody, FuncParam, FuncSig}, + item::{AdtDef, ContractItem, FunctionDef, Item, Module}, + ty::{TypeRef, TypeRefKind}, + }, + input::SourceFile, + nameres::{self as hir_nameres, DefResolutionKind, Resolution, TypeVarBinding}, +}; +use hir_ty::{TyKind, TyScheme}; +use lsp_types::{ + ParameterInformation, ParameterLabel, Position, SignatureHelp, SignatureInformation, Url, +}; + +use crate::{ + resolve::{function_owning_offset, module_id_for_uri}, + state::WorldState, +}; + +/// Computes signature help for the nearest call argument list at a source +/// position. +pub fn handle_signature_help( + world: &WorldState, + uri: &Url, + position: Position, +) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let offset = line_index.position_to_byte(position)?; + let current_module = module_id_for_uri(world, db, uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + let env = nameres::module_env(db, current_module); + + let owner = function_owning_offset(db, module, file, offset)?; + let body_map = body_resolution_map( + db, + module, + owner.function, + owner.root_body, + owner.enclosing_contract, + owner.inherited_type_vars, + &env, + ); + let call = enclosing_call(db, owner.root_body, file, offset, line_index.text())?; + let resolution = expr_resolution(&body_map, call.body, call.callee)?; + let mut signature = callable_signature(db, resolution)?; + let active_parameter = if signature.parameters.is_empty() { + 0 + } else { + call.active_parameter + .min(signature.parameters.len() as u32 - 1) + }; + + let parameters = signature + .parameters + .drain(..) + .map(|label| ParameterInformation { + label: ParameterLabel::Simple(label), + documentation: None, + }) + .collect::>(); + + Some(SignatureHelp { + signatures: vec![SignatureInformation { + label: signature.label, + documentation: None, + parameters: Some(parameters), + active_parameter: Some(active_parameter), + }], + active_signature: Some(0), + active_parameter: Some(active_parameter), + }) +} + +struct CallAtOffset<'db> { + body: FuncBody<'db>, + callee: Id>, + active_parameter: u32, +} + +struct CallableSignature { + label: String, + parameters: Vec, +} + +fn body_resolution_map<'db>( + db: &'db dyn hir_ty::Db, + module: Module<'db>, + function: FunctionDef<'db>, + root_body: FuncBody<'db>, + enclosing_contract: Option>, + mut type_vars: Vec>, + imports: &dyn hir_nameres::ImportedNames<'db>, +) -> hir_nameres::BodyResolutionMap<'db> { + let sig = function.sig(db); + type_vars.extend(hir_nameres::type_var_bindings( + function.def_id_value(db), + &sig.type_vars, + )); + let context = hir_nameres::BodyResolutionContext { + module, + enclosing_contract, + params: hir_nameres::param_bindings(sig.params.atom()), + type_vars, + }; + hir_nameres::resolve_body_with_imports_and_policy( + db, + root_body, + &context, + imports, + hir_nameres::NameresDiagnosticPolicy::Emit, + ) +} + +fn enclosing_call<'db>( + db: &'db dyn hir_ty::Db, + root_body: FuncBody<'db>, + file: SourceFile, + offset: u32, + text: &str, +) -> Option> { + let mut best = None; + let mut stack = vec![root_body]; + + while let Some(body) = stack.pop() { + for (expr_id, expr) in body.exprs(db).iter() { + if let ExprKind::Lambda { + body: lambda_body, .. + } = &expr.kind + { + stack.push(*lambda_body); + } + + let ExprKind::Call { callee, args } = &expr.kind else { + continue; + }; + let absolute = expr.span.resolve_to_absolute(db); + if absolute.file() != file + || offset < absolute.start().as_u32() + || absolute.end().as_u32() < offset + { + continue; + } + + let callee_span = body.exprs(db).get(*callee).span.resolve_to_absolute(db); + let Some((args_start, args_end)) = + call_argument_range(text, callee_span.end().as_u32(), absolute.end().as_u32()) + else { + continue; + }; + if offset < args_start || args_end < offset { + continue; + } + + let width = absolute.len(); + let active_parameter = active_parameter(db, body, args, file, offset, text, args_start); + if best + .as_ref() + .is_none_or(|(_, _, best_width)| width < *best_width) + { + best = Some(( + expr_id, + CallAtOffset { + body, + callee: *callee, + active_parameter, + }, + width, + )); + } + } + } + + best.map(|(_, call, _)| call) +} + +fn call_argument_range(text: &str, callee_end: u32, call_end: u32) -> Option<(u32, u32)> { + let bytes = text.as_bytes(); + let search_start = callee_end as usize; + let search_end = call_end.min(text.len() as u32) as usize; + let open = bytes + .get(search_start..search_end)? + .iter() + .position(|byte| *byte == b'(')? + + search_start; + + let mut depth = 0u32; + for (index, byte) in bytes.iter().enumerate().take(search_end).skip(open) { + match *byte { + b'(' => depth += 1, + b')' => { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(((open + 1) as u32, index as u32)); + } + } + _ => {} + } + } + + None +} + +fn active_parameter<'db>( + db: &'db dyn hir_ty::Db, + body: FuncBody<'db>, + args: &[Id>], + file: SourceFile, + offset: u32, + text: &str, + args_start: u32, +) -> u32 { + for (index, arg) in args.iter().enumerate() { + let absolute = body.exprs(db).get(*arg).span.resolve_to_absolute(db); + if absolute.file() == file + && absolute.start().as_u32() <= offset + && offset <= absolute.end().as_u32() + { + return index as u32; + } + } + + count_commas_before(text, args_start, offset) +} + +fn count_commas_before(text: &str, start: u32, offset: u32) -> u32 { + let start = start.min(text.len() as u32) as usize; + let end = offset.min(text.len() as u32) as usize; + let mut depth = 0u32; + let mut commas = 0u32; + + for byte in text.as_bytes()[start..end].iter().copied() { + match byte { + b'(' | b'[' | b'{' => depth += 1, + b')' | b']' | b'}' => depth = depth.saturating_sub(1), + b',' if depth == 0 => commas += 1, + _ => {} + } + } + + commas +} + +fn expr_resolution<'db>( + body_map: &hir_nameres::BodyResolutionMap<'db>, + body: FuncBody<'db>, + expr: Id>, +) -> Option> { + body_map + .exprs + .iter() + .find(|entry| entry.body == body && entry.expr == expr) + .map(|entry| entry.resolution.clone()) +} + +fn callable_signature<'db>( + db: &'db dyn hir_ty::Db, + resolution: Resolution<'db>, +) -> Option { + match resolution { + Resolution::Def { + def, + kind: DefResolutionKind::Function, + } => { + let function = function_for_def(db, def)?; + let sig = function.sig(db); + let name = sig.name.atom().text(db).to_owned(); + let defining_module = nameres::module_id_for_source_file(db, def.file(db))?; + let scheme = hir_ty::infer::function_scheme(db, defining_module, def)?; + signature_from_scheme(db, &name, &[], scheme, Some(sig), None) + } + Resolution::Ctor { ty, index } => { + let ctor = adt_ctor_for_def(db, ty, index.as_usize())?; + let name = ctor.name.atom().text(db).to_owned(); + let defining_module = nameres::module_id_for_source_file(db, ty.file(db))?; + let scheme = hir_ty::infer::adt_ctor_scheme(db, defining_module, ty, index)?; + let source_params = adt_ctor_source_params(db, &ctor); + signature_from_scheme(db, &name, &[], scheme, None, Some(&source_params)) + } + Resolution::ClassMethod { class, name } => { + let defining_module = nameres::module_id_for_source_file(db, class.file(db))?; + let scheme = + hir_ty::infer::class_method_scheme(db, defining_module, class, name.clone())?; + let source_sig = class_method_sig_for_def(db, class, &name); + signature_from_scheme(db, &name, &[], scheme, source_sig, None) + } + Resolution::Builtin(kind) => { + let name = builtin_name(kind)?; + let scheme = hir_ty::builtin_scheme(db, kind)?; + signature_from_scheme(db, name, &[], scheme, None, None) + } + Resolution::Def { .. } + | Resolution::Local(_) + | Resolution::Param(_) + | Resolution::Field(_) + | Resolution::Module(_) + | Resolution::DotCtorDeferred + | Resolution::Err => None, + } +} + +fn signature_from_scheme<'db>( + db: &'db dyn hir_ty::Db, + name: &str, + param_names: &[String], + scheme: TyScheme<'db>, + source_sig: Option<&FuncSig<'db>>, + source_params: Option<&[TypeRef<'db>]>, +) -> Option { + let ty = scheme.body(db).ty(db); + let (params, ret) = match ty.kind(db) { + TyKind::Function { params, ret } => (params.clone(), *ret), + _ => (Vec::new(), ty), + }; + let parameters = params + .iter() + .enumerate() + .map(|(index, param)| { + let source_param = source_sig.and_then(|sig| sig.params.atom().get(index)); + let ty = source_param + .and_then(|param| match param { + FuncParam::Typed { ty, .. } => Some(hir_ty::display_type_ref_source(db, *ty)), + FuncParam::Untyped { .. } | FuncParam::Error { .. } => None, + }) + .or_else(|| { + source_params + .and_then(|params| params.get(index)) + .map(|ty| hir_ty::display_type_ref_source(db, *ty)) + }) + .unwrap_or_else(|| param.display(db)); + if let Some( + FuncParam::Typed { comptime, name, .. } | FuncParam::Untyped { comptime, name }, + ) = source_param + { + let prefix = if comptime.is_some() { "comptime " } else { "" }; + return format!("{prefix}{}: {ty}", name.atom().text(db)); + } + param_names + .get(index) + .map(|name| format!("{name}: {ty}")) + .unwrap_or(ty) + }) + .collect::>(); + let return_suffix = source_sig + .and_then(|sig| sig.ret) + .map(|ret| display_source_return_suffix(db, ret)) + .unwrap_or_else(|| match ret.kind(db) { + TyKind::Tuple(elements) if elements.is_empty() => String::new(), + TyKind::Tuple(elements) => format!( + " returns ({})", + elements + .iter() + .map(|element| element.display(db)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", ret.display(db)), + }); + let label = format!("{name}({}){return_suffix}", parameters.join(", ")); + + Some(CallableSignature { label, parameters }) +} + +fn function_for_def<'db>(db: &'db dyn hir_ty::Db, def: DefId<'db>) -> Option> { + let file = def.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + module + .items(db) + .iter() + .find_map(|item| function_in_item(db, *item, def)) +} + +fn function_in_item<'db>( + db: &'db dyn hir_ty::Db, + item: Item<'db>, + def: DefId<'db>, +) -> Option> { + match item { + Item::FunctionDef(function) if function.def_id_value(db) == def => Some(function), + Item::InstanceDef(instance) => instance + .methods(db) + .iter() + .copied() + .find(|method| method.def_id_value(db) == def), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match *item { + ContractItem::FunctionDef(function) if function.def_id_value(db) == def => { + Some(function) + } + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }), + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => None, + } +} + +fn class_method_sig_for_def<'db>( + db: &'db dyn hir_ty::Db, + def: DefId<'db>, + name: &str, +) -> Option<&'db FuncSig<'db>> { + let file = def.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + module.items(db).iter().find_map(|item| match item { + Item::ClassDef(class) if class.def_id_value(db) == def => class + .methods(db) + .iter() + .find(|method| method.name.atom().text(db) == name), + _ => None, + }) +} + +fn display_source_return_suffix<'db>(db: &'db dyn hir_ty::Db, ret: TypeRef<'db>) -> String { + match ret.kind(db) { + TypeRefKind::Tuple { elems } if elems.atom().is_empty() => String::new(), + TypeRefKind::Tuple { elems } => format!( + " returns ({})", + elems + .atom() + .iter() + .map(|elem| hir_ty::display_type_ref_source(db, *elem)) + .collect::>() + .join(", ") + ), + _ => format!(" returns ({})", hir_ty::display_type_ref_source(db, ret)), + } +} + +fn adt_ctor_for_def<'db>( + db: &'db dyn hir_ty::Db, + def: DefId<'db>, + index: usize, +) -> Option> { + let file = def.file(db); + let module = parser::parse_file_to_hir(db, file).module(db); + let adt = module + .items(db) + .iter() + .find_map(|item| adt_in_item(db, *item, def))?; + adt.ctors(db).get(index).cloned() +} + +fn adt_ctor_source_params<'db>( + db: &'db dyn hir_ty::Db, + ctor: &hir::ast::item::AdtCtor<'db>, +) -> Vec> { + let fields = *ctor.fields.atom(); + match ctor.field_count { + 0 => Vec::new(), + 1 => vec![fields], + _ => match fields.kind(db) { + TypeRefKind::Tuple { elems } => elems.atom().clone(), + _ => vec![fields], + }, + } +} + +fn adt_in_item<'db>( + db: &'db dyn hir_ty::Db, + item: Item<'db>, + def: DefId<'db>, +) -> Option> { + match item { + Item::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match *item { + ContractItem::AdtDef(adt) if adt.def_id_value(db) == def => Some(adt), + ContractItem::FunctionDef(_) + | ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => None, + }), + Item::FunctionDef(_) + | Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => None, + } +} + +fn builtin_name(kind: hir_nameres::BuiltinKind) -> Option<&'static str> { + Some(match kind { + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::True) => "true", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::False) => "false", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Unit) => "()", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Pair) => "pair", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Inl) => "inl", + hir_nameres::BuiltinKind::Constructor(hir_nameres::BuiltinCtor::Inr) => "inr", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::Invoke) => "invoke", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimAddWord) => { + "primAddWord" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimEqWord) => { + "primEqWord" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordToInteger) => { + "wordToInteger" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordFromInteger) => { + "wordFromInteger" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerAdd) => { + "integerAdd" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerSub) => { + "integerSub" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerMul) => { + "integerMul" + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerLt) => "integerLt", + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerEq) => "integerEq", + hir_nameres::BuiltinKind::ClassMethod(hir_nameres::BuiltinClassMethod::InvokableInvoke) => { + "invoke" + } + hir_nameres::BuiltinKind::ClassMethod(hir_nameres::BuiltinClassMethod::IntFromInteger) => { + "fromInteger" + } + hir_nameres::BuiltinKind::Type(_) | hir_nameres::BuiltinKind::Class(_) => return None, + }) +} + +#[cfg(test)] +mod tests { + use lsp_types::Url; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + fn position_at(source: &str, world: &WorldState, uri: &Url, needle: &str) -> Position { + let offset = source.find(needle).expect("needle") as u32; + world + .line_index(uri) + .expect("line index") + .byte_to_position(offset) + } + + #[test] + fn highlights_first_argument() { + let source = "function f(a: word, b: word) returns (word) {\n return a;\n}\n\nfunction main() returns (word) {\n return f(1, 2);\n}\n"; + let (world, uri) = world_with_main(source); + let position = position_at(source, &world, &uri, "1, 2"); + + let help = handle_signature_help(&world, &uri, position).expect("signature help"); + + assert_eq!(help.active_signature, Some(0)); + assert_eq!(help.active_parameter, Some(0)); + assert_eq!(help.signatures[0].active_parameter, Some(0)); + } + + #[test] + fn highlights_second_argument_and_labels_signature() { + let source = "function f(a: word, b: word) returns (word) {\n return a;\n}\n\nfunction main() returns (word) {\n return f(1, 2);\n}\n"; + let (world, uri) = world_with_main(source); + let comma_offset = source.find(", 2").expect("comma") as u32 + 1; + let position = world + .line_index(&uri) + .expect("line index") + .byte_to_position(comma_offset); + + let help = handle_signature_help(&world, &uri, position).expect("signature help"); + let signature = &help.signatures[0]; + + assert_eq!(help.active_parameter, Some(1)); + assert_eq!(signature.active_parameter, Some(1)); + assert!( + signature.label.contains("f("), + "expected function name in label, got {}", + signature.label + ); + assert!( + signature.label.contains("a: word"), + "expected first parameter in label, got {}", + signature.label + ); + assert!( + signature.label.contains("b: word"), + "expected second parameter in label, got {}", + signature.label + ); + assert!( + signature.label.contains("returns (word)"), + "expected return type in label, got {}", + signature.label + ); + } + + #[test] + fn signature_help_uses_requested_module_when_unrelated_document_opened_first() { + let unrelated = "function unrelated() returns (word) { return 0; }\n"; + let main = "function combine(a: word, b: word) returns (word) { return a; }\n\nfunction main() returns (word) {\n return combine(1, 2);\n}\n"; + let unrelated_uri = Url::parse("file:///main/unrelated.solc").expect("unrelated uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let mut world = WorldState::new(); + assert!(world.open_document(unrelated_uri, unrelated.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let comma_offset = main.find(", 2").expect("comma") as u32 + 1; + let position = world + .line_index(&main_uri) + .expect("line index") + .byte_to_position(comma_offset); + + let help = + handle_signature_help(&world, &main_uri, position).expect("signature help response"); + let signature = &help.signatures[0]; + + assert_eq!(help.active_parameter, Some(1)); + assert!( + signature.label.contains("combine("), + "expected imported function signature, got {}", + signature.label + ); + assert!(signature.label.contains("a: word")); + assert!(signature.label.contains("b: word")); + } + + #[test] + fn signature_help_resolves_imported_function_in_defining_module() { + let unrelated = "function unrelated() returns (word) { return 0; }\n"; + let math = "function combine(a: word, b: word) returns (word) { return a; }\n\nexport { combine };\n"; + let main = "import {combine} from math;\n\nfunction main() returns (word) {\n return combine(1, 2);\n}\n"; + let unrelated_uri = Url::parse("file:///main/unrelated.solc").expect("unrelated uri"); + let math_uri = Url::parse("file:///main/math.solc").expect("math uri"); + let main_uri = Url::parse("file:///main/main.solc").expect("main uri"); + let mut world = WorldState::new(); + assert!(world.open_document(unrelated_uri, unrelated.to_owned())); + assert!(world.open_document(math_uri, math.to_owned())); + assert!(world.open_document(main_uri.clone(), main.to_owned())); + let comma_offset = main.find(", 2").expect("comma") as u32 + 1; + let position = world + .line_index(&main_uri) + .expect("line index") + .byte_to_position(comma_offset); + + let help = + handle_signature_help(&world, &main_uri, position).expect("signature help response"); + let signature = &help.signatures[0]; + + assert_eq!(help.active_parameter, Some(1)); + assert!(signature.label.contains("combine(")); + assert!(signature.label.contains("a: word")); + assert!(signature.label.contains("b: word")); + } + + #[test] + fn signature_help_preserves_function_type_qualifiers() { + let source = "\ +function register( + comptime callback: function(word) external view returns (bool), + seed: word +) returns (bool) { + return true; +} + +function main() returns (bool) { + return register(1, 2); +} +"; + let (world, uri) = world_with_main(source); + let position = position_at(source, &world, &uri, "1, 2"); + let help = handle_signature_help(&world, &uri, position).expect("signature help"); + let signature = &help.signatures[0]; + + assert_eq!( + signature.label, + "register(comptime callback: function(word) external view returns (bool), seed: word) returns (bool)" + ); + assert_eq!( + signature.parameters.as_ref().expect("parameters")[0].label, + ParameterLabel::Simple( + "comptime callback: function(word) external view returns (bool)".to_owned() + ) + ); + } + + #[test] + fn constructor_signature_help_preserves_function_type_qualifiers() { + let source = "\ +enum CallbackBox { CallbackBox(function(word) external view returns (bool)) } + +function main() returns (CallbackBox) { + return CallbackBox.CallbackBox(1); +} +"; + let (world, uri) = world_with_main(source); + let position = position_at(source, &world, &uri, "1);"); + let help = handle_signature_help(&world, &uri, position).expect("signature help"); + let signature = &help.signatures[0]; + + assert_eq!( + signature.label, + "CallbackBox(function(word) external view returns (bool)) returns (adt:CallbackBox)" + ); + assert_eq!( + signature.parameters.as_ref().expect("parameters")[0].label, + ParameterLabel::Simple("function(word) external view returns (bool)".to_owned()) + ); + } +} diff --git a/crates/lsp/src/state.rs b/crates/lsp/src/state.rs new file mode 100644 index 00000000..20ac25a7 --- /dev/null +++ b/crates/lsp/src/state.rs @@ -0,0 +1,1316 @@ +//! In-memory LSP document state over `solcore-vfs`. +//! +//! The compiler VFS uses `/main/` for user files. Browser clients may +//! use those virtual URIs directly, while native clients map real workspace +//! file URIs to `/main` and back for cross-file editor results. + +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; + +use lsp_types::{TextDocumentContentChangeEvent, Url}; +use percent_encoding::percent_decode_str; +use vfs::{AnalysisHost, Workspace, WorkspaceFileChange}; + +use crate::{analysis::with_analysis_stack, line_index::LineIndexExt}; + +const MULTI_ROOT_NAMESPACE_DIR: &str = "__solcore_workspace__"; +const DETACHED_NAMESPACE_DIR: &str = "__solcore_detached__"; +const DETACHED_NAMESPACE_PREFIX: &str = "/main/__solcore_detached__/"; + +/// A known text document and its position mapper. +#[derive(Debug)] +pub struct DocumentState { + line_index: LineIndexExt, +} + +impl DocumentState { + /// Builds document state for full-text LSP synchronization. + pub fn new(text: String) -> Self { + Self { + line_index: LineIndexExt::new(&text), + } + } + + /// Returns the current document text. + pub fn text(&self) -> &str { + self.line_index.text() + } + + /// Returns the current UTF-8/UTF-16 mapper. + pub fn line_index(&self) -> &LineIndexExt { + &self.line_index + } +} + +/// Transport-independent LSP world state. +pub struct WorldState { + workspace: Workspace, + documents: HashMap, + open_documents: HashSet, + client_to_vfs: HashMap, + vfs_to_client: HashMap, + workspace_roots: Vec, + workspace_namespaced: bool, + next_virtual_document_id: u64, + entry_uri: Option, +} + +/// A client workspace folder and its collision-free virtual namespace. +#[derive(Clone, Debug, PartialEq, Eq)] +struct WorkspaceRoot { + uri: Url, + identity: String, + namespace: String, +} + +impl WorkspaceRoot { + fn new(uri: Url) -> Option { + let identity = workspace_root_identity(&uri)?; + let namespace = hex_encode(identity.as_bytes()); + Some(Self { + uri, + identity, + namespace, + }) + } +} + +impl WorldState { + /// Creates an empty world with the embedded standard library mounted. + pub fn new() -> Self { + Self { + workspace: with_analysis_stack(Workspace::new), + documents: HashMap::new(), + open_documents: HashSet::new(), + client_to_vfs: HashMap::new(), + vfs_to_client: HashMap::new(), + workspace_roots: Vec::new(), + workspace_namespaced: false, + next_virtual_document_id: 0, + entry_uri: None, + } + } + + /// Mounts all known Solcore files below a client workspace root. + /// + /// Client file URIs are translated to stable `/main/` VFS + /// paths. The files remain available for imports and cross-file navigation + /// even when they are not open in the editor. + pub fn load_workspace_documents( + &mut self, + root: Url, + files: impl IntoIterator, + ) -> usize { + self.load_workspace_roots([(root, files.into_iter().collect())]) + } + + /// Mounts all Solcore files from every client workspace folder. + /// + /// A single folder keeps the traditional `/main/` layout. + /// With multiple folders, each folder receives a stable namespace below + /// `/main/__solcore_workspace__/`, so equal relative paths never overwrite + /// one another while relative imports stay inside their originating folder. + pub fn load_workspace_roots( + &mut self, + roots: impl IntoIterator)>, + ) -> usize { + let mut root_uris = Vec::new(); + let mut files = Vec::new(); + for (root, root_files) in roots { + root_uris.push(root); + files.extend(root_files); + } + + self.replace_workspace_roots(root_uris); + + self.load_documents(files) + } + + /// Applies a dynamic `workspace/didChangeWorkspaceFolders` update. + /// + /// Closed documents below removed roots are discarded. Open documents are + /// retained under a collision-free detached namespace until they close or + /// their root is added again. + pub fn update_workspace_roots( + &mut self, + removed: impl IntoIterator, + added: impl IntoIterator)>, + ) -> (usize, Vec) { + let removed = removed + .into_iter() + .filter_map(|uri| workspace_root_identity(&uri)) + .collect::>(); + let known_before = self.documents.keys().cloned().collect::>(); + let mut roots = self + .workspace_roots + .iter() + .filter(|root| !removed.contains(&root.identity)) + .map(|root| root.uri.clone()) + .collect::>(); + let mut files = Vec::new(); + for (root, root_files) in added { + roots.push(root); + files.extend(root_files); + } + + self.replace_workspace_roots(roots); + let loaded = self.load_documents(files); + let mut discarded = known_before + .into_iter() + .filter(|uri| !self.documents.contains_key(uri)) + .collect::>(); + discarded.sort_by(|left, right| left.as_str().cmp(right.as_str())); + (loaded, discarded) + } + + fn load_documents(&mut self, files: impl IntoIterator) -> usize { + let mut changes = Vec::new(); + let mut seen = HashSet::new(); + let mut loaded = 0; + + for (uri, text) in files { + if !seen.insert(uri.clone()) || self.open_documents.contains(&uri) { + continue; + } + let Some(path) = self.ensure_uri_mapping(&uri) else { + continue; + }; + changes.push(WorkspaceFileChange::Set { + path, + contents: text.clone(), + }); + self.documents.insert(uri, DocumentState::new(text)); + loaded += 1; + } + + with_analysis_stack(|| self.workspace.apply_file_changes(changes)); + loaded + } + + /// Adds or refreshes a closed workspace file, for example after a watched + /// file-system event. + pub fn set_workspace_document(&mut self, uri: Url, text: String) -> bool { + let Some(path) = self.ensure_uri_mapping(&uri) else { + return false; + }; + with_analysis_stack(|| self.workspace.set_file(&path, text.clone())); + self.documents.insert(uri, DocumentState::new(text)); + true + } + + /// Removes a closed workspace file from the analysis graph. + pub fn remove_workspace_document(&mut self, uri: &Url) -> bool { + if self.open_documents.contains(uri) { + return false; + } + let Some(path) = self.client_to_vfs.remove(uri) else { + return false; + }; + if let Some(key) = vfs_url_for_path(&path) { + self.vfs_to_client.remove(&key); + } + self.documents.remove(uri); + with_analysis_stack(|| self.workspace.remove_file(&path)); + true + } + + /// Opens a full-text document under `/main`. + /// + /// Returns `false` for out-of-workspace URIs. + pub fn open_document(&mut self, uri: Url, text: String) -> bool { + let Some(path) = self.ensure_uri_mapping(&uri) else { + return false; + }; + + with_analysis_stack(|| self.workspace.set_file(&path, text.clone())); + if self.entry_uri.is_none() { + with_analysis_stack(|| self.workspace.set_entry(&path)); + self.entry_uri = Some(uri.clone()); + } + self.documents.insert(uri.clone(), DocumentState::new(text)); + self.open_documents.insert(uri); + true + } + + /// Applies a full-text document change under `/main`. + /// + /// Returns `false` for out-of-workspace URIs. + pub fn change_document(&mut self, uri: &Url, new_text: String) -> bool { + let Some(path) = self.ensure_uri_mapping(uri) else { + return false; + }; + + with_analysis_stack(|| self.workspace.set_file(&path, new_text.clone())); + if self.entry_uri.is_none() { + with_analysis_stack(|| self.workspace.set_entry(&path)); + self.entry_uri = Some(uri.clone()); + } + self.documents + .insert(uri.clone(), DocumentState::new(new_text)); + self.open_documents.insert(uri.clone()); + true + } + + /// Applies an LSP content-change batch in protocol order. + /// + /// Full-document and ranged changes may be mixed. The update is atomic: + /// an invalid range leaves the current document unchanged. + pub fn apply_document_changes( + &mut self, + uri: &Url, + changes: Vec, + ) -> bool { + let Some(mut text) = self.document_text(uri).map(str::to_owned) else { + return false; + }; + + for change in changes { + let Some(range) = change.range else { + text = change.text; + continue; + }; + let line_index = LineIndexExt::new(&text); + let Some(start) = line_index.position_to_byte(range.start) else { + return false; + }; + let Some(end) = line_index.position_to_byte(range.end) else { + return false; + }; + if start > end { + return false; + } + let Some(replaced) = text.get(start as usize..end as usize) else { + return false; + }; + if change.range_length.is_some_and(|range_length| { + replaced.encode_utf16().count() != range_length as usize + }) { + return false; + } + text.replace_range(start as usize..end as usize, &change.text); + } + + self.change_document(uri, text) + } + + /// Closes a document in the LSP layer. + /// + /// The VFS file and line index are kept so imports and navigation remain + /// stable. Native transports may refresh the retained text from disk. + pub fn close_document(&mut self, uri: &Url) { + self.open_documents.remove(uri); + if self.entry_uri.as_ref() == Some(uri) { + self.entry_uri = self + .open_documents + .iter() + .min_by(|left, right| left.as_str().cmp(right.as_str())) + .cloned(); + if let Some(entry_uri) = &self.entry_uri + && let Some(path) = self.vfs_path_for_uri(entry_uri) + { + with_analysis_stack(|| self.workspace.set_entry(&path)); + } + } + } + + /// Returns the current text for an open document. + pub fn document_text(&self, uri: &Url) -> Option<&str> { + self.open_documents + .contains(uri) + .then(|| self.documents.get(uri).map(DocumentState::text)) + .flatten() + } + + /// Returns whether a client document is currently open. + pub fn is_document_open(&self, uri: &Url) -> bool { + self.open_documents.contains(uri) + } + + /// Returns whether a URI currently belongs to a configured workspace + /// folder rather than merely being retained as an open detached document. + pub fn is_uri_in_workspace(&self, uri: &Url) -> bool { + if self + .client_to_vfs + .get(uri) + .is_some_and(|path| path.starts_with(DETACHED_NAMESPACE_PREFIX)) + { + return false; + } + self.workspace_relative_vfs_path(uri).is_some() || uri_to_vfs_path(uri).is_some() + } + + /// Returns whether real client file URIs have a configured workspace root. + pub fn has_workspace_root(&self) -> bool { + !self.workspace_roots.is_empty() + } + + /// Returns the number of configured client workspace folders. + pub fn workspace_root_count(&self) -> usize { + self.workspace_roots.len() + } + + /// Returns the URIs for currently open documents in deterministic order. + pub fn open_document_uris(&self) -> Vec { + let mut uris = self.open_documents.iter().cloned().collect::>(); + uris.sort_by(|left, right| left.as_str().cmp(right.as_str())); + uris + } + + /// Returns every file known to the workspace in deterministic order. + pub fn workspace_document_uris(&self) -> Vec { + let mut uris = self.documents.keys().cloned().collect::>(); + uris.sort_by(|left, right| left.as_str().cmp(right.as_str())); + uris + } + + /// Returns the current line index for any known workspace document. + pub fn line_index(&self, uri: &Url) -> Option<&LineIndexExt> { + self.documents.get(uri).map(DocumentState::line_index) + } + + /// Maps a client document URI to its virtual `/main` VFS path. + pub fn vfs_path_for_uri(&self, uri: &Url) -> Option { + self.client_to_vfs + .get(uri) + .cloned() + .or_else(|| self.workspace_relative_vfs_path(uri)) + .or_else(|| uri_to_vfs_path(uri)) + } + + /// Maps a VFS source-file URL back to the URI understood by the client. + pub fn client_uri_for_vfs_url(&self, vfs_url: &str) -> Option { + let uri = Url::parse(vfs_url).ok()?; + self.vfs_to_client.get(uri.as_str()).cloned().or(Some(uri)) + } + + /// Returns the underlying in-memory workspace. + pub fn workspace(&self) -> &Workspace { + &self.workspace + } + + /// Returns the underlying Salsa analysis database. + pub fn db(&self) -> &AnalysisHost { + self.workspace.db() + } + + fn ensure_uri_mapping(&mut self, uri: &Url) -> Option { + if let Some(path) = self.client_to_vfs.get(uri) { + return Some(path.clone()); + } + + if self.workspace_roots.is_empty() + && uri.scheme() == "file" + && uri_to_vfs_path(uri).is_none() + && let Ok(root) = uri.join(".") + && let Some(root) = WorkspaceRoot::new(root) + { + self.workspace_roots.push(root); + } + + let path = self + .workspace_relative_vfs_path(uri) + .or_else(|| uri_to_vfs_path(uri)) + .or_else(|| self.virtual_document_path(uri))?; + self.client_to_vfs.insert(uri.clone(), path.clone()); + self.vfs_to_client + .insert(vfs_url_for_path(&path)?, uri.clone()); + Some(path) + } + + fn workspace_relative_vfs_path(&self, uri: &Url) -> Option { + let (root, relative) = self + .workspace_roots + .iter() + .filter_map(|root| { + workspace_relative_path(&root.uri, uri).map(|relative| (root, relative)) + }) + .max_by(|(left, _), (right, _)| { + workspace_root_path_len(&left.uri) + .cmp(&workspace_root_path_len(&right.uri)) + .then_with(|| right.identity.cmp(&left.identity)) + })?; + let relative = relative_url_path(&relative)?; + if self.workspace_roots.len() == 1 && !self.workspace_namespaced { + Some(format!("/main/{relative}")) + } else { + Some(format!( + "/main/{MULTI_ROOT_NAMESPACE_DIR}/{}/{relative}", + root.namespace + )) + } + } + + fn replace_workspace_roots(&mut self, roots: impl IntoIterator) { + let mut roots = roots + .into_iter() + .filter_map(WorkspaceRoot::new) + .collect::>(); + roots.sort_by(|left, right| left.identity.cmp(&right.identity)); + roots.dedup_by(|left, right| left.identity == right.identity); + + if self + .workspace_roots + .iter() + .map(|root| &root.identity) + .eq(roots.iter().map(|root| &root.identity)) + { + return; + } + + let has_detached_open_document = self.open_documents.iter().any(|uri| { + let belonged_to_old_root = self + .workspace_roots + .iter() + .any(|root| workspace_relative_path(&root.uri, uri).is_some()); + let belongs_to_new_root = roots + .iter() + .any(|root| workspace_relative_path(&root.uri, uri).is_some()); + uri.scheme() == "file" + && (uri_to_vfs_path(uri).is_none() || belonged_to_old_root) + && !belongs_to_new_root + }); + self.workspace_namespaced |= + roots.len() > 1 || (!roots.is_empty() && has_detached_open_document); + + let real_documents = self + .documents + .iter() + .filter(|(uri, _)| { + uri.scheme() == "file" + && (uri_to_vfs_path(uri).is_none() + || self + .workspace_roots + .iter() + .any(|root| workspace_relative_path(&root.uri, uri).is_some()) + || roots + .iter() + .any(|root| workspace_relative_path(&root.uri, uri).is_some())) + }) + .map(|(uri, document)| { + let detached_path = self + .client_to_vfs + .get(uri) + .filter(|path| path.starts_with(DETACHED_NAMESPACE_PREFIX)) + .cloned() + .or_else(|| self.detached_vfs_path(uri)); + ( + uri.clone(), + document.text().to_owned(), + self.open_documents.contains(uri), + detached_path, + ) + }) + .collect::>(); + let mut changes = Vec::new(); + for (uri, _, _, _) in &real_documents { + if let Some(path) = self.client_to_vfs.remove(uri) { + if let Some(key) = vfs_url_for_path(&path) { + self.vfs_to_client.remove(&key); + } + changes.push(WorkspaceFileChange::Remove { path }); + } + } + + self.workspace_roots = roots; + for (uri, text, is_open, detached_path) in real_documents { + let Some(path) = self + .workspace_relative_vfs_path(&uri) + .or_else(|| is_open.then_some(detached_path).flatten()) + else { + self.documents.remove(&uri); + self.open_documents.remove(&uri); + continue; + }; + self.client_to_vfs.insert(uri.clone(), path.clone()); + if let Some(key) = vfs_url_for_path(&path) { + self.vfs_to_client.insert(key, uri); + } + changes.push(WorkspaceFileChange::Set { + path, + contents: text, + }); + } + with_analysis_stack(|| self.workspace.apply_file_changes(changes)); + self.refresh_entry(); + } + + fn detached_vfs_path(&self, uri: &Url) -> Option { + let (namespace, relative) = self + .workspace_roots + .iter() + .filter_map(|root| { + workspace_relative_path(&root.uri, uri).map(|relative| (root, relative)) + }) + .max_by(|(left, _), (right, _)| { + workspace_root_path_len(&left.uri) + .cmp(&workspace_root_path_len(&right.uri)) + .then_with(|| right.identity.cmp(&left.identity)) + }) + .and_then(|(root, relative)| { + relative_url_path(&relative).map(|relative| (root.namespace.to_owned(), relative)) + }) + .unwrap_or_else(|| { + let identity = workspace_root_identity(uri).unwrap_or_else(|| uri.to_string()); + let filename = Path::new(uri.path()) + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| !name.is_empty()) + .unwrap_or("document.solc") + .to_owned(); + (hex_encode(identity.as_bytes()), filename) + }); + Some(format!( + "/main/{DETACHED_NAMESPACE_DIR}/{namespace}/{relative}" + )) + } + + fn refresh_entry(&mut self) { + if self + .entry_uri + .as_ref() + .is_some_and(|uri| self.open_documents.contains(uri)) + && let Some(path) = self + .entry_uri + .as_ref() + .and_then(|uri| self.vfs_path_for_uri(uri)) + { + with_analysis_stack(|| self.workspace.set_entry(&path)); + return; + } + + self.entry_uri = self + .open_documents + .iter() + .filter(|uri| self.vfs_path_for_uri(uri).is_some()) + .min_by(|left, right| left.as_str().cmp(right.as_str())) + .cloned(); + if let Some(path) = self + .entry_uri + .as_ref() + .and_then(|uri| self.vfs_path_for_uri(uri)) + { + with_analysis_stack(|| self.workspace.set_entry(&path)); + } + } + + fn virtual_document_path(&mut self, uri: &Url) -> Option { + if uri.scheme() == "file" { + return None; + } + let id = self.next_virtual_document_id; + self.next_virtual_document_id += 1; + let extension = Path::new(uri.path()) + .extension() + .and_then(|extension| extension.to_str()) + .filter(|extension| !extension.is_empty()) + .unwrap_or("solc"); + Some(format!("/main/__virtual__/{id}.{extension}")) + } +} + +impl Default for WorldState { + fn default() -> Self { + Self::new() + } +} + +/// Maps a client `file:///main/` URI to a VFS path. +pub fn uri_to_vfs_path(uri: &Url) -> Option { + if uri.scheme() != "file" { + return None; + } + let path = percent_decode_str(uri.path()).decode_utf8().ok()?; + path.starts_with("/main/").then(|| path.into_owned()) +} + +/// Maps a VFS source-file URL string to the client URI used by LSP. +pub fn vfs_url_to_client_uri(vfs_url: &str) -> Option { + Url::parse(vfs_url).ok() +} + +fn workspace_root_identity(uri: &Url) -> Option { + let path = normalized_url_path(uri)?; + let path = if path == "/" { + path.as_str() + } else { + path.trim_end_matches('/') + }; + Some(format!( + "{}\0{}\0{}\0{path}", + uri.scheme(), + uri.host_str().unwrap_or_default(), + uri.port().map_or_else(String::new, |port| port.to_string()), + )) +} + +fn workspace_relative_path(root: &Url, uri: &Url) -> Option { + if uri.scheme() != root.scheme() + || uri.host_str() != root.host_str() + || uri.port() != root.port() + { + return None; + } + + let root_path = normalized_url_path(root)?; + let file_path = normalized_url_path(uri)?; + let root_path = root_path.trim_end_matches('/'); + if root_path.is_empty() { + return file_path + .strip_prefix('/') + .filter(|relative| !relative.is_empty()) + .map(str::to_owned); + } + + let mut prefix = root_path.to_owned(); + prefix.push('/'); + file_path.strip_prefix(&prefix).map(str::to_owned) +} + +fn workspace_root_path_len(root: &Url) -> usize { + normalized_url_path(root) + .map(|path| path.trim_end_matches('/').len()) + .unwrap_or_default() +} + +fn normalized_url_path(uri: &Url) -> Option { + let mut path = decoded_url_path(uri)?; + let bytes = path.as_bytes(); + if uri.scheme() == "file" + && bytes.len() >= 3 + && bytes[0] == b'/' + && bytes[1].is_ascii_alphabetic() + && bytes[2] == b':' + { + let drive = (bytes[1] as char).to_ascii_uppercase().to_string(); + path.replace_range(1..2, &drive); + } + Some(path) +} + +fn relative_url_path(path: &str) -> Option { + let mut segments = Vec::new(); + for segment in path.split('/') { + match segment { + "" | "." => {} + ".." => return None, + segment => segments.push(segment), + } + } + (!segments.is_empty()).then(|| segments.join("/")) +} + +fn hex_encode(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut encoded = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + encoded.push(HEX[(byte >> 4) as usize] as char); + encoded.push(HEX[(byte & 0x0f) as usize] as char); + } + encoded +} + +fn vfs_url_for_path(path: &str) -> Option { + let mut url = Url::parse("file:///").ok()?; + url.set_path(path); + Some(url.into()) +} + +fn decoded_url_path(uri: &Url) -> Option { + percent_decode_str(uri.path()) + .decode_utf8() + .ok() + .map(|path| path.into_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::GotoDefinitionResponse; + + #[test] + fn maps_main_file_uris_to_vfs_paths() { + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert_eq!(uri_to_vfs_path(&uri), Some("/main/main.solc".to_owned())); + + let std_uri = Url::parse("file:///std/std.solc").expect("uri"); + assert_eq!(uri_to_vfs_path(&std_uri), None); + + let memory_uri = Url::parse("memory:///main/main.solc").expect("uri"); + assert_eq!(uri_to_vfs_path(&memory_uri), None); + } + + #[test] + fn open_change_and_close_document() { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + let clean = "function main() returns (word) {\n return 1;\n}\n"; + let changed = "function main() returns (word) {\n return 2;\n}\n"; + + assert!(world.open_document(uri.clone(), clean.to_owned())); + assert_eq!(world.document_text(&uri), Some(clean)); + assert!(world.change_document(&uri, changed.to_owned())); + assert_eq!(world.document_text(&uri), Some(changed)); + + world.close_document(&uri); + assert_eq!(world.document_text(&uri), None); + assert!(world.line_index(&uri).is_some()); + } + + #[test] + fn content_change_batches_apply_utf16_ranges_in_order_atomically() { + use lsp_types::{Position, Range}; + + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), "a😀c\n".to_owned())); + + assert!(world.apply_document_changes( + &uri, + vec![ + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 1), Position::new(0, 3))), + range_length: Some(2), + text: "β".to_owned(), + }, + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 2), Position::new(0, 2))), + range_length: Some(0), + text: "!".to_owned(), + }, + ], + )); + assert_eq!(world.document_text(&uri), Some("aβ!c\n")); + + assert!(!world.apply_document_changes( + &uri, + vec![TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(99, 0), Position::new(99, 1))), + range_length: None, + text: "corrupt".to_owned(), + }], + )); + assert_eq!(world.document_text(&uri), Some("aβ!c\n")); + } + + #[test] + fn real_workspace_uris_round_trip_through_virtual_main_paths() { + let mut world = WorldState::new(); + let root_path = std::env::temp_dir().join("solcore-lsp-state-project"); + let root = Url::from_directory_path(&root_path).expect("root uri"); + let main_uri = Url::from_file_path(root_path.join("src/main.solc")).expect("main uri"); + let util_uri = Url::from_file_path(root_path.join("src/util.solc")).expect("util uri"); + + assert_eq!( + world.load_workspace_documents( + root, + [ + ( + main_uri.clone(), + "function main() returns (word) { return 1; }\n".to_owned() + ), + ( + util_uri.clone(), + "function util() returns (word) { return 2; }\n".to_owned() + ), + ], + ), + 2 + ); + assert_eq!( + world.vfs_path_for_uri(&main_uri), + Some("/main/src/main.solc".to_owned()) + ); + assert_eq!( + world.client_uri_for_vfs_url("file:///main/src/util.solc"), + Some(util_uri) + ); + assert!(world.open_document( + main_uri.clone(), + "function main() returns (word) { return 1; }\n".to_owned() + )); + assert_eq!(world.open_document_uris(), vec![main_uri]); + assert_eq!(world.workspace_document_uris().len(), 2); + } + + #[test] + fn encoded_real_workspace_uris_round_trip() { + let mut world = WorldState::new(); + let root_path = std::env::temp_dir().join("solcore-lsp-state-encoded-project"); + let root = Url::from_directory_path(&root_path).expect("root uri"); + let uri = Url::from_file_path(root_path.join("src/数 学.solc")).expect("encoded uri"); + assert_eq!( + world.load_workspace_documents( + root, + [( + uri.clone(), + "function value() returns (word) { return 1; }\n".to_owned() + )] + ), + 1 + ); + assert_eq!( + world.vfs_path_for_uri(&uri), + Some("/main/src/数 学.solc".to_owned()) + ); + assert_eq!( + world.client_uri_for_vfs_url("file:///main/src/%E6%95%B0%20%E5%AD%A6.solc"), + Some(uri) + ); + } + + #[test] + fn multiple_workspace_roots_are_isolated_and_stable() { + let base = std::env::temp_dir().join("solcore-lsp-state-multi-root"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root uri"); + let right_root = Url::from_directory_path(&right_path).expect("right root uri"); + let left_uri = Url::from_file_path(left_path.join("src/main.solc")).expect("left uri"); + let right_uri = Url::from_file_path(right_path.join("src/main.solc")).expect("right uri"); + let source = "function value() returns (word) { return 1; }\n"; + + let mut world = WorldState::new(); + assert_eq!( + world.load_workspace_roots([ + ( + left_root.clone(), + vec![(left_uri.clone(), source.to_owned())] + ), + ( + right_root.clone(), + vec![(right_uri.clone(), source.to_owned())] + ), + ]), + 2 + ); + + let left_vfs = world.vfs_path_for_uri(&left_uri).expect("left vfs path"); + let right_vfs = world.vfs_path_for_uri(&right_uri).expect("right vfs path"); + assert!(left_vfs.starts_with("/main/__solcore_workspace__/")); + assert!(right_vfs.starts_with("/main/__solcore_workspace__/")); + assert!(left_vfs.ends_with("/src/main.solc")); + assert!(right_vfs.ends_with("/src/main.solc")); + assert_ne!(left_vfs, right_vfs); + assert_eq!(world.workspace_root_count(), 2); + assert_eq!( + world.client_uri_for_vfs_url(&vfs_url_for_path(&left_vfs).expect("left vfs url")), + Some(left_uri.clone()) + ); + assert_eq!( + world.client_uri_for_vfs_url(&vfs_url_for_path(&right_vfs).expect("right vfs url")), + Some(right_uri.clone()) + ); + + let mut reordered = WorldState::new(); + reordered.load_workspace_roots([ + (right_root, vec![(right_uri.clone(), source.to_owned())]), + (left_root, vec![(left_uri.clone(), source.to_owned())]), + ]); + assert_eq!(reordered.vfs_path_for_uri(&left_uri), Some(left_vfs)); + assert_eq!(reordered.vfs_path_for_uri(&right_uri), Some(right_vfs)); + } + + #[test] + fn configured_main_file_root_uses_multi_root_namespace_before_virtual_mapping() { + let main_root = Url::parse("file:///main/").expect("main root"); + let other_root = Url::parse("file:///workspace/other/").expect("other root"); + let main_uri = Url::parse("file:///main/project.solc").expect("main uri"); + let other_uri = Url::parse("file:///workspace/other/project.solc").expect("other uri"); + let mut world = WorldState::new(); + + world.load_workspace_roots([ + ( + main_root, + vec![(main_uri.clone(), "function left() {}\n".to_owned())], + ), + ( + other_root, + vec![(other_uri.clone(), "function right() {}\n".to_owned())], + ), + ]); + + let main_path = world.vfs_path_for_uri(&main_uri).expect("main path"); + let other_path = world.vfs_path_for_uri(&other_uri).expect("other path"); + assert!(main_path.starts_with("/main/__solcore_workspace__/")); + assert!(other_path.starts_with("/main/__solcore_workspace__/")); + assert_ne!(main_path, other_path); + } + + #[test] + fn rootless_main_document_is_remapped_when_workspace_folders_arrive() { + let main_root = Url::parse("file:///main/").expect("main root"); + let other_root = Url::parse("file:///workspace/other/").expect("other root"); + let main_uri = Url::parse("file:///main/project.solc").expect("main uri"); + let mut world = WorldState::new(); + assert!(world.open_document( + main_uri.clone(), + "function value() returns (word) { return 1; }\n".to_owned() + )); + assert_eq!( + world.vfs_path_for_uri(&main_uri), + Some("/main/project.solc".to_owned()) + ); + + world.update_workspace_roots( + Vec::::new(), + [(main_root, Vec::new()), (other_root, Vec::new())], + ); + + assert!( + world + .vfs_path_for_uri(&main_uri) + .is_some_and(|path| path.starts_with("/main/__solcore_workspace__/")) + ); + assert!(world.is_uri_in_workspace(&main_uri)); + } + + #[test] + fn multi_root_imports_and_workspace_symbols_use_the_originating_root() { + let base = std::env::temp_dir().join("solcore-lsp-state-multi-root-resolution"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root uri"); + let right_root = Url::from_directory_path(&right_path).expect("right root uri"); + let left_main = Url::from_file_path(left_path.join("main.solc")).expect("left main uri"); + let left_math = Url::from_file_path(left_path.join("math.solc")).expect("left math uri"); + let right_main = Url::from_file_path(right_path.join("main.solc")).expect("right main uri"); + let right_math = Url::from_file_path(right_path.join("math.solc")).expect("right math uri"); + let left_source = "import {leftValue} from lib.math;\nfunction runLeft() returns (word) { return leftValue(); }\n"; + let left_library = + "function leftValue() returns (word) { return 1; }\nexport { leftValue };\n"; + let right_source = "import {rightValue} from lib.math;\nfunction runRight() returns (word) { return rightValue(); }\n"; + let right_library = + "function rightValue() returns (word) { return 2; }\nexport { rightValue };\n"; + + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root, + vec![ + (left_main.clone(), left_source.to_owned()), + (left_math.clone(), left_library.to_owned()), + ], + ), + ( + right_root, + vec![ + (right_main, right_source.to_owned()), + (right_math.clone(), right_library.to_owned()), + ], + ), + ]); + assert!(world.open_document(left_main.clone(), left_source.to_owned())); + + let use_offset = left_source.rfind("leftValue").expect("left value use") as u32; + let position = world + .line_index(&left_main) + .expect("left main line index") + .byte_to_position(use_offset); + let definition = crate::definition::handle_definition(&world, &left_main, position) + .expect("cross-file definition"); + let GotoDefinitionResponse::Scalar(location) = definition else { + panic!("expected scalar definition"); + }; + assert_eq!(location.uri, left_math); + + let symbols = crate::workspace_symbols::handle_workspace_symbol(&world, "Value") + .expect("workspace symbols"); + assert!( + symbols + .iter() + .any(|symbol| symbol.name == "leftValue" && symbol.location.uri == left_math) + ); + assert!( + symbols + .iter() + .any(|symbol| symbol.name == "rightValue" && symbol.location.uri == right_math) + ); + } + + #[test] + fn watched_files_are_updated_in_their_own_workspace_namespace() { + let base = std::env::temp_dir().join("solcore-lsp-state-multi-root-watch"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root uri"); + let right_root = Url::from_directory_path(&right_path).expect("right root uri"); + let left_uri = Url::from_file_path(left_path.join("shared.solc")).expect("left uri"); + let right_uri = Url::from_file_path(right_path.join("shared.solc")).expect("right uri"); + let generated_uri = + Url::from_file_path(right_path.join("generated.solc")).expect("generated uri"); + + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root, + vec![(left_uri.clone(), "function left() {}\n".to_owned())], + ), + ( + right_root, + vec![(right_uri.clone(), "function right() {}\n".to_owned())], + ), + ]); + assert!(world.set_workspace_document( + generated_uri.clone(), + "function generated() {}\n".to_owned() + )); + + let generated_vfs = world + .vfs_path_for_uri(&generated_uri) + .expect("generated vfs path"); + let right_vfs = world.vfs_path_for_uri(&right_uri).expect("right vfs path"); + let left_vfs = world.vfs_path_for_uri(&left_uri).expect("left vfs path"); + assert_eq!( + generated_vfs.rsplit_once('/').map(|(parent, _)| parent), + right_vfs.rsplit_once('/').map(|(parent, _)| parent) + ); + assert_ne!( + generated_vfs.rsplit_once('/').map(|(parent, _)| parent), + left_vfs.rsplit_once('/').map(|(parent, _)| parent) + ); + assert!(world.remove_workspace_document(&generated_uri)); + assert!(world.line_index(&generated_uri).is_none()); + assert!(world.line_index(&left_uri).is_some()); + assert!(world.line_index(&right_uri).is_some()); + } + + #[test] + fn dynamic_root_removal_discards_closed_files_and_detaches_open_files() { + let base = std::env::temp_dir().join("solcore-lsp-state-dynamic-roots"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let left_root = Url::from_directory_path(&left_path).expect("left root uri"); + let right_root = Url::from_directory_path(&right_path).expect("right root uri"); + let left_main = Url::from_file_path(left_path.join("main.solc")).expect("left main uri"); + let left_util = Url::from_file_path(left_path.join("util.solc")).expect("left util uri"); + let right_main = Url::from_file_path(right_path.join("main.solc")).expect("right main uri"); + let disk_source = "function value() returns (word) { return 1; }\n"; + let unsaved_source = "function value() returns (word) { return 99; }\n"; + + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root.clone(), + vec![ + (left_main.clone(), disk_source.to_owned()), + (left_util.clone(), "function util() {}\n".to_owned()), + ], + ), + ( + right_root, + vec![(right_main.clone(), "function right() {}\n".to_owned())], + ), + ]); + assert!(world.open_document(left_main.clone(), unsaved_source.to_owned())); + + let (loaded, discarded) = world.update_workspace_roots( + [left_root.clone()], + std::iter::empty::<(Url, Vec<(Url, String)>)>(), + ); + assert_eq!(loaded, 0); + assert_eq!(discarded, vec![left_util.clone()]); + assert_eq!(world.workspace_root_count(), 1); + assert_eq!(world.document_text(&left_main), Some(unsaved_source)); + assert!(!world.is_uri_in_workspace(&left_main)); + assert!( + world + .vfs_path_for_uri(&left_main) + .is_some_and(|path| path.starts_with("/main/__solcore_detached__/")) + ); + assert!(world.line_index(&left_util).is_none()); + assert!(world.is_uri_in_workspace(&right_main)); + assert!( + world + .vfs_path_for_uri(&right_main) + .is_some_and(|path| path.starts_with("/main/__solcore_workspace__/")) + ); + + let (loaded, discarded) = world.update_workspace_roots( + Vec::::new(), + [( + left_root.clone(), + vec![ + (left_main.clone(), disk_source.to_owned()), + (left_util.clone(), "function util() {}\n".to_owned()), + ], + )], + ); + assert_eq!(loaded, 1, "the open editor buffer must not be overwritten"); + assert!(discarded.is_empty()); + assert_eq!(world.workspace_root_count(), 2); + assert!(world.is_uri_in_workspace(&left_main)); + assert_eq!(world.document_text(&left_main), Some(unsaved_source)); + assert!(world.line_index(&left_util).is_some()); + assert!( + world + .vfs_path_for_uri(&left_main) + .is_some_and(|path| path.starts_with("/main/__solcore_workspace__/")) + ); + + world.update_workspace_roots([left_root], std::iter::empty::<(Url, Vec<(Url, String)>)>()); + world.close_document(&left_main); + assert!(world.remove_workspace_document(&left_main)); + assert!(world.line_index(&left_main).is_none()); + } + + #[test] + fn detached_files_keep_root_isolation_and_relative_layout_across_folder_changes() { + let base = std::env::temp_dir().join("solcore-lsp-state-detached-isolation"); + let left_path = base.join("left"); + let right_path = base.join("right"); + let third_path = base.join("third"); + let left_root = Url::from_directory_path(&left_path).expect("left root"); + let right_root = Url::from_directory_path(&right_path).expect("right root"); + let third_root = Url::from_directory_path(&third_path).expect("third root"); + let left_main = Url::from_file_path(left_path.join("main.solc")).expect("left main"); + let left_math = Url::from_file_path(left_path.join("math.solc")).expect("left math"); + let right_math = Url::from_file_path(right_path.join("math.solc")).expect("right math"); + let third_file = Url::from_file_path(third_path.join("third.solc")).expect("third file"); + let main_source = "import {leftValue} from lib.math;\nfunction main() returns (word) { return leftValue(); }\n"; + let left_source = + "function leftValue() returns (word) { return 1; }\nexport { leftValue };\n"; + let right_source = + "function rightValue() returns (word) { return 2; }\nexport { rightValue };\n"; + + let mut world = WorldState::new(); + world.load_workspace_roots([ + ( + left_root.clone(), + vec![ + (left_main.clone(), main_source.to_owned()), + (left_math.clone(), left_source.to_owned()), + ], + ), + (right_root, vec![(right_math, right_source.to_owned())]), + ]); + assert!(world.open_document(left_main.clone(), main_source.to_owned())); + assert!(world.open_document(left_math.clone(), left_source.to_owned())); + world.update_workspace_roots([left_root], std::iter::empty::<(Url, Vec<(Url, String)>)>()); + + let detached_main = world + .vfs_path_for_uri(&left_main) + .expect("detached main path"); + let detached_math = world + .vfs_path_for_uri(&left_math) + .expect("detached math path"); + assert!(detached_main.starts_with("/main/__solcore_detached__/")); + assert_eq!( + detached_main.rsplit_once('/').map(|(parent, _)| parent), + detached_math.rsplit_once('/').map(|(parent, _)| parent) + ); + assert_definition_uri(&world, &left_main, main_source, "leftValue", &left_math); + + world.update_workspace_roots( + Vec::::new(), + [( + third_root, + vec![( + third_file, + "function third() returns (word) { return 3; }\n".to_owned(), + )], + )], + ); + assert_eq!(world.vfs_path_for_uri(&left_main), Some(detached_main)); + assert_eq!(world.vfs_path_for_uri(&left_math), Some(detached_math)); + assert_definition_uri(&world, &left_main, main_source, "leftValue", &left_math); + } + + fn assert_definition_uri( + world: &WorldState, + uri: &Url, + source: &str, + name: &str, + expected: &Url, + ) { + let offset = source.rfind(name).expect("reference") as u32; + let position = world + .line_index(uri) + .expect("line index") + .byte_to_position(offset); + let definition = crate::definition::handle_definition(world, uri, position) + .expect("definition response"); + let GotoDefinitionResponse::Scalar(location) = definition else { + panic!("expected scalar definition"); + }; + assert_eq!(&location.uri, expected); + } + + #[test] + fn file_uri_drive_letters_are_normalized_without_folding_path_case() { + let root = Url::parse("file:///c:/CaseSensitive/Project").expect("root uri"); + let matching = + Url::parse("file:///C:/CaseSensitive/Project/main.solc").expect("matching uri"); + let wrong_case = + Url::parse("file:///C:/casesensitive/Project/main.solc").expect("wrong-case uri"); + + assert_eq!( + workspace_relative_path(&root, &matching).as_deref(), + Some("main.solc") + ); + assert_eq!(workspace_relative_path(&root, &wrong_case), None); + } + + #[test] + fn first_real_document_infers_its_parent_as_workspace_root() { + let mut world = WorldState::new(); + let file = std::env::temp_dir() + .join("solcore-lsp-inferred-root") + .join("main.solc"); + let uri = Url::from_file_path(file).expect("real file uri"); + + assert!(world.open_document( + uri.clone(), + "function main() returns (word) { return 1; }\n".to_owned() + )); + + assert!(world.has_workspace_root()); + assert_eq!( + world.vfs_path_for_uri(&uri), + Some("/main/main.solc".to_owned()) + ); + } + + #[test] + fn untitled_documents_receive_stable_virtual_paths() { + let mut world = WorldState::new(); + let uri = Url::parse("untitled:Untitled-1").expect("untitled uri"); + assert!(world.open_document( + uri.clone(), + "function main() returns (word) { return 1; }\n".to_owned() + )); + assert_eq!( + world.vfs_path_for_uri(&uri), + Some("/main/__virtual__/0.solc".to_owned()) + ); + assert_eq!( + world.client_uri_for_vfs_url("file:///main/__virtual__/0.solc"), + Some(uri) + ); + } + + #[test] + fn closed_virtual_documents_can_be_discarded() { + let mut world = WorldState::new(); + let uri = Url::parse("untitled:Untitled-1").expect("untitled uri"); + assert!(world.open_document( + uri.clone(), + "function main() returns (word) { return 1; }\n".to_owned() + )); + + world.close_document(&uri); + assert!(world.remove_workspace_document(&uri)); + assert!(world.line_index(&uri).is_none()); + assert!(world.vfs_path_for_uri(&uri).is_none()); + } + + #[cfg(not(windows))] + #[test] + fn unix_backslash_in_filename_does_not_become_a_path_separator() { + assert_eq!( + relative_url_path("src/name\\part.solc"), + Some("src/name\\part.solc".to_owned()) + ); + } +} diff --git a/crates/lsp/src/symbols.rs b/crates/lsp/src/symbols.rs new file mode 100644 index 00000000..0c071c8e --- /dev/null +++ b/crates/lsp/src/symbols.rs @@ -0,0 +1,322 @@ +//! Document symbol support over the wasm-clean LSP core. + +use hir::{ + ast::item::{ContractItem, FieldDef, FuncKind, InstanceDef, Item}, + span::{Span, Spanned}, +}; +use lsp_types::{DocumentSymbol, DocumentSymbolResponse, Range, SymbolKind, Url}; + +use crate::state::WorldState; + +/// Computes hierarchical symbols for one open source document. +pub fn handle_document_symbol(world: &WorldState, uri: &Url) -> Option { + let db = world.db(); + let path = world.vfs_path_for_uri(uri)?; + let file = db.source_file(&path)?; + let line_index = world.line_index(uri)?; + let module = parser::parse_file_to_hir(db, file).module(db); + + let symbols = module + .items(db) + .iter() + .filter_map(|item| symbol_for_item(db, line_index, *item)) + .collect::>(); + + Some(DocumentSymbolResponse::Nested(symbols)) +} + +fn symbol_for_item<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + item: Item<'db>, +) -> Option { + match item { + Item::FunctionDef(function) => Some(document_symbol( + db, + line_index, + function.sig(db).name.atom().text(db).to_owned(), + SymbolKind::FUNCTION, + function.span(db), + function.sig(db).name.span(db), + None, + )), + Item::TypeAlias(alias) => Some(document_symbol( + db, + line_index, + alias.name_elem(db).atom().text(db).to_owned(), + SymbolKind::CLASS, + alias.span(db), + alias.name_elem(db).span(db), + None, + )), + Item::AdtDef(adt) => Some(document_symbol( + db, + line_index, + adt.name_elem(db).atom().text(db).to_owned(), + SymbolKind::ENUM, + adt.span(db), + adt.name_elem(db).span(db), + None, + )), + Item::ClassDef(class) => { + let name = class.head(db).kind(db).class; + Some(document_symbol( + db, + line_index, + name.atom().text(db).to_owned(), + SymbolKind::INTERFACE, + class.span(db), + name.span(db), + None, + )) + } + Item::InstanceDef(instance) => instance_symbol(db, line_index, instance), + Item::ContractDef(contract) => { + let mut children = contract + .fields(db) + .iter() + .map(|field| field_symbol(db, line_index, field)) + .collect::>(); + children.extend( + contract + .items(db) + .iter() + .filter_map(|item| symbol_for_contract_item(db, line_index, *item)), + ); + Some(document_symbol( + db, + line_index, + contract.name_elem(db).atom().text(db).to_owned(), + contract_symbol_kind(contract.kind(db)), + contract.span(db), + contract.name_elem(db).span(db), + Some(children), + )) + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => None, + } +} + +fn contract_symbol_kind(kind: hir::ast::item::ContractKind) -> SymbolKind { + match kind { + hir::ast::item::ContractKind::Contract => SymbolKind::CLASS, + hir::ast::item::ContractKind::Interface => SymbolKind::INTERFACE, + hir::ast::item::ContractKind::Library => SymbolKind::MODULE, + } +} + +fn symbol_for_contract_item<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + item: ContractItem<'db>, +) -> Option { + match item { + ContractItem::FunctionDef(function) => { + let kind = match function.kind(db) { + FuncKind::Constructor => SymbolKind::CONSTRUCTOR, + FuncKind::Function | FuncKind::Fallback => SymbolKind::METHOD, + }; + Some(document_symbol( + db, + line_index, + function.sig(db).name.atom().text(db).to_owned(), + kind, + function.span(db), + function.sig(db).name.span(db), + None, + )) + } + ContractItem::TypeAlias(alias) => Some(document_symbol( + db, + line_index, + alias.name_elem(db).atom().text(db).to_owned(), + SymbolKind::CLASS, + alias.span(db), + alias.name_elem(db).span(db), + None, + )), + ContractItem::AdtDef(adt) => Some(document_symbol( + db, + line_index, + adt.name_elem(db).atom().text(db).to_owned(), + SymbolKind::ENUM, + adt.span(db), + adt.name_elem(db).span(db), + None, + )), + ContractItem::Error { .. } => None, + } +} + +fn field_symbol<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + field: &FieldDef<'db>, +) -> DocumentSymbol { + document_symbol( + db, + line_index, + field.name().atom().text(db).to_owned(), + SymbolKind::FIELD, + field.span(db), + field.name().span(db), + None, + ) +} + +fn instance_symbol<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + instance: InstanceDef<'db>, +) -> Option { + let head = instance.head(db); + let class = head.kind(db).class; + Some(document_symbol( + db, + line_index, + format!("impl {}", class.atom().text(db)), + SymbolKind::OBJECT, + instance.span(db), + class.span(db), + None, + )) +} + +fn document_symbol<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + name: String, + kind: SymbolKind, + range_span: Span<'db>, + selection_span: Span<'db>, + children: Option>, +) -> DocumentSymbol { + #[allow(deprecated)] + let symbol = DocumentSymbol { + name, + detail: None, + kind, + tags: None, + deprecated: None, + range: lsp_range(db, line_index, range_span), + selection_range: lsp_range(db, line_index, selection_span), + children: children.filter(|children| !children.is_empty()), + }; + symbol +} + +fn lsp_range<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + span: Span<'db>, +) -> Range { + let absolute = span.resolve_to_absolute(db); + line_index.range(absolute.start().as_u32(), absolute.end().as_u32()) +} + +#[cfg(test)] +mod tests { + use lsp_types::Position; + + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn document_symbols_include_top_level_items_and_contract_children() { + let source = "\ +function foo(x: word) returns (word) { + return x; +} + +alias Pair = pair; + +enum Maybe { None, Some(word) } + +contract Box { + item: word; + function get() returns (word) { + return item; + } +} + +interface Reader { + function read(key: word) external view returns (word); +} + +library Helpers { + function identity(value: word) internal pure returns (word) { return value; } +} +"; + let (world, uri) = world_with_main(source); + let response = handle_document_symbol(&world, &uri).expect("symbols"); + let DocumentSymbolResponse::Nested(symbols) = response else { + panic!("expected nested document symbols"); + }; + + let foo = find_symbol(&symbols, "foo").expect("foo symbol"); + assert_eq!(foo.kind, SymbolKind::FUNCTION); + assert_selection_in_range(foo); + + let pair = find_symbol(&symbols, "Pair").expect("Pair symbol"); + assert_eq!(pair.kind, SymbolKind::CLASS); + assert_selection_in_range(pair); + + let maybe = find_symbol(&symbols, "Maybe").expect("Maybe symbol"); + assert_eq!(maybe.kind, SymbolKind::ENUM); + assert_selection_in_range(maybe); + + let contract = find_symbol(&symbols, "Box").expect("Box symbol"); + assert_eq!(contract.kind, SymbolKind::CLASS); + assert_selection_in_range(contract); + let children = contract.children.as_ref().expect("contract children"); + assert_eq!( + find_symbol(children, "item").expect("field").kind, + SymbolKind::FIELD + ); + assert_eq!( + find_symbol(children, "get").expect("method").kind, + SymbolKind::METHOD + ); + for child in children { + assert_selection_in_range(child); + } + + assert_eq!( + find_symbol(&symbols, "Reader") + .expect("interface symbol") + .kind, + SymbolKind::INTERFACE + ); + assert_eq!( + find_symbol(&symbols, "Helpers") + .expect("library symbol") + .kind, + SymbolKind::MODULE + ); + } + + fn find_symbol<'a>(symbols: &'a [DocumentSymbol], name: &str) -> Option<&'a DocumentSymbol> { + symbols.iter().find(|symbol| symbol.name == name) + } + + fn assert_selection_in_range(symbol: &DocumentSymbol) { + assert!( + position_le(symbol.range.start, symbol.selection_range.start) + && position_le(symbol.selection_range.end, symbol.range.end), + "selection range {:?} must be contained in {:?} for {}", + symbol.selection_range, + symbol.range, + symbol.name + ); + } + + fn position_le(left: Position, right: Position) -> bool { + left.line < right.line || (left.line == right.line && left.character <= right.character) + } +} diff --git a/crates/lsp/src/wasm.rs b/crates/lsp/src/wasm.rs new file mode 100644 index 00000000..95c7fb9e --- /dev/null +++ b/crates/lsp/src/wasm.rs @@ -0,0 +1,1525 @@ +//! WASM Web Worker entry for JSON-RPC over `postMessage`. +//! +//! The browser worker transport delivers one JSON-RPC object per message, so +//! this module intentionally does not implement `Content-Length` framing. + +use lsp_types::{ + CodeActionParams, CompletionParams, DidChangeTextDocumentParams, + DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + DocumentFormattingParams, DocumentHighlightParams, DocumentSymbolParams, FoldingRangeParams, + GotoDefinitionParams, HoverParams, InitializeParams, InlayHintParams, ReferenceParams, + RenameParams, SelectionRangeParams, SemanticTokensParams, SignatureHelpParams, + TextDocumentPositionParams, WorkspaceSymbolParams, +}; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::state::WorldState; + +const PARSE_ERROR: i64 = -32700; +const INVALID_REQUEST: i64 = -32600; +const METHOD_NOT_FOUND: i64 = -32601; +const INVALID_PARAMS: i64 = -32602; + +#[cfg(feature = "wasm")] +#[wasm_bindgen] +pub struct SolcoreLsp { + world: WorldState, +} + +#[cfg(feature = "wasm")] +#[wasm_bindgen] +impl SolcoreLsp { + #[wasm_bindgen(constructor)] + pub fn new() -> SolcoreLsp { + SolcoreLsp { + world: WorldState::new(), + } + } + + /// Handle one incoming JSON-RPC 2.0 message encoded as a JSON string. + /// + /// Returns JSON strings for outgoing messages: the response first, followed + /// by any `textDocument/publishDiagnostics` notifications. + pub fn handle(&mut self, message: String) -> Vec { + dispatch(&mut self.world, &message) + } +} + +#[cfg(feature = "wasm")] +impl Default for SolcoreLsp { + fn default() -> Self { + Self::new() + } +} + +pub(crate) fn dispatch(world: &mut WorldState, message: &str) -> Vec { + let Ok(value) = serde_json::from_str::(message) else { + return vec![error_response(Value::Null, PARSE_ERROR, "Parse error")]; + }; + + let id = request_id(&value); + let Some(method) = value + .as_object() + .and_then(|object| object.get("method")) + .and_then(Value::as_str) + else { + return error_or_empty(id, INVALID_REQUEST, "Invalid Request"); + }; + let params = value + .as_object() + .and_then(|object| object.get("params")) + .cloned() + .unwrap_or(Value::Null); + + match method { + "initialize" => handle_initialize(world, id, params), + "initialized" | "exit" => Vec::new(), + "shutdown" => null_response_or_empty(id), + method if method.starts_with("$/") => Vec::new(), + "textDocument/didOpen" => handle_did_open(world, id, params), + "textDocument/didChange" => handle_did_change(world, id, params), + "textDocument/didClose" => handle_did_close(world, id, params), + "workspace/didChangeWorkspaceFolders" => { + handle_did_change_workspace_folders(world, id, params) + } + "textDocument/completion" => handle_completion_request(world, id, params), + "textDocument/hover" => handle_hover_request(world, id, params), + "textDocument/signatureHelp" => handle_signature_help_request(world, id, params), + "textDocument/definition" => handle_definition_request(world, id, params), + "textDocument/references" => handle_references_request(world, id, params), + "textDocument/rename" => handle_rename_request(world, id, params), + "textDocument/prepareRename" => handle_prepare_rename_request(world, id, params), + "textDocument/documentHighlight" => handle_document_highlight_request(world, id, params), + "textDocument/documentSymbol" => handle_document_symbol_request(world, id, params), + "textDocument/codeAction" => handle_code_action_request(world, id, params), + "textDocument/formatting" => handle_formatting_request(world, id, params), + "textDocument/foldingRange" => handle_folding_range_request(world, id, params), + "textDocument/selectionRange" => handle_selection_range_request(world, id, params), + "textDocument/semanticTokens/full" => { + handle_semantic_tokens_full_request(world, id, params) + } + "textDocument/inlayHint" => handle_inlay_hints_request(world, id, params), + "workspace/symbol" => handle_workspace_symbol_request(world, id, params), + _ => error_or_empty(id, METHOD_NOT_FOUND, "Method not found"), + } +} + +fn handle_initialize(world: &mut WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + let roots = initial_workspace_roots(¶ms) + .into_iter() + .map(|root| (root, Vec::new())); + world.load_workspace_roots(roots); + vec![result_response( + id, + crate::capabilities::initialize_result(), + )] +} + +fn request_id(value: &Value) -> Option { + value + .as_object() + .and_then(|object| object.get("id")) + .cloned() +} + +fn handle_did_open(world: &mut WorldState, id: Option, params: Value) -> Vec { + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return error_or_empty(id, INVALID_PARAMS, "Invalid params"), + }; + + let uri = params.text_document.uri; + world.open_document(uri.clone(), params.text_document.text); + + let mut outgoing = null_response_or_empty(id); + outgoing.extend(publish_open_document_diagnostics(world)); + outgoing +} + +fn handle_did_change(world: &mut WorldState, id: Option, params: Value) -> Vec { + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return error_or_empty(id, INVALID_PARAMS, "Invalid params"), + }; + + let uri = params.text_document.uri; + if params.content_changes.is_empty() { + return null_response_or_empty(id); + } + + if !world.apply_document_changes(&uri, params.content_changes) { + return error_or_empty(id, INVALID_PARAMS, "Invalid content change"); + } + + let mut outgoing = null_response_or_empty(id); + outgoing.extend(publish_open_document_diagnostics(world)); + outgoing +} + +fn handle_did_close(world: &mut WorldState, id: Option, params: Value) -> Vec { + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return error_or_empty(id, INVALID_PARAMS, "Invalid params"), + }; + + let uri = params.text_document.uri; + world.close_document(&uri); + world.remove_workspace_document(&uri); + + let mut outgoing = null_response_or_empty(id); + outgoing.push(publish_diagnostics(uri, Vec::new())); + outgoing.extend(publish_open_document_diagnostics(world)); + outgoing +} + +fn handle_did_change_workspace_folders( + world: &mut WorldState, + id: Option, + params: Value, +) -> Vec { + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return error_or_empty(id, INVALID_PARAMS, "Invalid params"), + }; + let removed = params.event.removed.into_iter().map(|folder| folder.uri); + let added = params + .event + .added + .into_iter() + .map(|folder| (folder.uri, Vec::new())); + let (_, discarded) = world.update_workspace_roots(removed, added); + + let mut outgoing = null_response_or_empty(id); + outgoing.extend( + discarded + .into_iter() + .map(|uri| publish_diagnostics(uri, Vec::new())), + ); + outgoing.extend(publish_open_document_diagnostics(world)); + outgoing +} + +fn handle_completion_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + vec![result_response( + id, + crate::completion::handle_completion(world, &uri, position), + )] +} + +fn handle_hover_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + vec![result_response( + id, + crate::hover::handle_hover(world, &uri, position), + )] +} + +fn handle_definition_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + vec![result_response( + id, + crate::definition::handle_definition(world, &uri, position), + )] +} + +fn handle_signature_help_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + vec![result_response( + id, + crate::signature_help::handle_signature_help(world, &uri, position), + )] +} + +fn handle_references_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let include_declaration = params.context.include_declaration; + vec![result_response( + id, + crate::references::handle_references(world, &uri, position, include_declaration), + )] +} + +fn handle_rename_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position.text_document.uri; + let position = params.text_document_position.position; + vec![result_response( + id, + crate::rename::handle_rename(world, &uri, position, ¶ms.new_name), + )] +} + +fn handle_prepare_rename_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document.uri; + let position = params.position; + vec![result_response( + id, + crate::rename::handle_prepare_rename(world, &uri, position), + )] +} + +fn handle_document_highlight_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + vec![result_response( + id, + crate::document_highlight::handle_document_highlight(world, &uri, position), + )] +} + +fn handle_document_symbol_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document.uri; + vec![result_response( + id, + crate::symbols::handle_document_symbol(world, &uri), + )] +} + +fn handle_code_action_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + vec![result_response( + id, + crate::code_actions::handle_code_action( + world, + ¶ms.text_document.uri, + params.range, + ¶ms.context, + ), + )] +} + +fn handle_formatting_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + vec![result_response( + id, + crate::formatting::handle_formatting(world, ¶ms.text_document.uri, ¶ms.options), + )] +} + +fn handle_folding_range_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + vec![result_response( + id, + crate::folding::handle_folding_range(world, ¶ms.text_document.uri), + )] +} + +fn handle_selection_range_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + vec![result_response( + id, + crate::selection_range::handle_selection_range( + world, + ¶ms.text_document.uri, + ¶ms.positions, + ), + )] +} + +fn handle_semantic_tokens_full_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document.uri; + vec![result_response( + id, + crate::semantic_tokens::handle_semantic_tokens_full(world, &uri), + )] +} + +fn handle_inlay_hints_request(world: &WorldState, id: Option, params: Value) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + let uri = params.text_document.uri; + vec![result_response( + id, + crate::inlay_hints::handle_inlay_hints(world, &uri, params.range), + )] +} + +fn handle_workspace_symbol_request( + world: &WorldState, + id: Option, + params: Value, +) -> Vec { + let Some(id) = id else { + return Vec::new(); + }; + let params = match deserialize_params::(params) { + Ok(params) => params, + Err(_) => return vec![error_response(id, INVALID_PARAMS, "Invalid params")], + }; + + vec![result_response( + id, + crate::workspace_symbols::handle_workspace_symbol(world, ¶ms.query), + )] +} + +#[allow(deprecated)] +fn initial_workspace_roots(params: &InitializeParams) -> Vec { + params + .workspace_folders + .as_ref() + .filter(|folders| !folders.is_empty()) + .map(|folders| folders.iter().map(|folder| folder.uri.clone()).collect()) + .unwrap_or_else(|| params.root_uri.clone().into_iter().collect()) +} + +fn deserialize_params(params: Value) -> Result { + serde_json::from_value(params) +} + +fn null_response_or_empty(id: Option) -> Vec { + id.map(|id| vec![result_response(id, Value::Null)]) + .unwrap_or_default() +} + +fn error_or_empty(id: Option, code: i64, message: &str) -> Vec { + id.map(|id| vec![error_response(id, code, message)]) + .unwrap_or_default() +} + +fn result_response(id: Value, result: T) -> String { + json_string(json!({ + "jsonrpc": "2.0", + "id": id, + "result": result, + })) +} + +fn error_response(id: Value, code: i64, message: &str) -> String { + json_string(json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": code, + "message": message, + }, + })) +} + +fn publish_diagnostics(uri: lsp_types::Url, diagnostics: Vec) -> String { + json_string(json!({ + "jsonrpc": "2.0", + "method": "textDocument/publishDiagnostics", + "params": { + "uri": uri, + "diagnostics": diagnostics, + }, + })) +} + +fn publish_open_document_diagnostics(world: &WorldState) -> Vec { + crate::diagnostics::compute_open_document_diagnostics(world) + .into_iter() + .map(|(uri, diagnostics)| publish_diagnostics(uri, diagnostics)) + .collect() +} + +fn json_string(value: Value) -> String { + serde_json::to_string(&value).unwrap_or_else(|_| { + r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Internal error"}}"# + .to_owned() + }) +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + + const URI: &str = "file:///main/main.solc"; + const MATH_URI: &str = "file:///main/math.solc"; + + #[test] + fn initialize_returns_capabilities_response() { + let mut world = WorldState::new(); + let outgoing = dispatch( + &mut world, + r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}}"#, + ); + + assert_eq!(outgoing.len(), 1); + let response = parse_message(&outgoing[0]); + assert_eq!(response["jsonrpc"], "2.0"); + assert_eq!(response["id"], 1); + assert_eq!( + response["result"], + serde_json::to_value(crate::capabilities::initialize_result()) + .expect("initialize result serializes") + ); + } + + #[test] + fn initialize_and_workspace_folder_changes_configure_multiple_roots() { + let mut world = WorldState::new(); + let left = "file:///workspace/left/"; + let right = "file:///workspace/right/"; + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "capabilities": {}, + "workspaceFolders": [{ "uri": left, "name": "left" }] + } + }) + .to_string(), + ); + assert_eq!(world.workspace_root_count(), 1); + + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWorkspaceFolders", + "params": { + "event": { + "added": [{ "uri": right, "name": "right" }], + "removed": [] + } + } + }) + .to_string(), + ); + assert_eq!(world.workspace_root_count(), 2); + + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWorkspaceFolders", + "params": { + "event": { + "added": [], + "removed": [{ "uri": left, "name": "left" }] + } + } + }) + .to_string(), + ); + assert_eq!(world.workspace_root_count(), 1); + } + + #[test] + fn did_open_publishes_diagnostics() { + let mut world = WorldState::new(); + let source = "function f() returns (word) {\n return true;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + + assert_eq!(outgoing.len(), 1); + let notification = parse_message(&outgoing[0]); + assert_eq!(notification["method"], "textDocument/publishDiagnostics"); + assert_eq!(notification["params"]["uri"], URI); + let diagnostics = notification["params"]["diagnostics"] + .as_array() + .expect("diagnostics array"); + assert!( + !diagnostics.is_empty(), + "expected at least one diagnostic, got {notification:#?}" + ); + } + + #[test] + fn did_change_republishes_importer_diagnostics_when_sibling_exports_change() { + let mut world = WorldState::new(); + let main = "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n"; + let math_no_export = "function double(x: word) returns (word) { return x; }\n"; + let math_with_export = + "function double(x: word) returns (word) { return x; }\n\nexport { double };\n"; + + let _ = dispatch(&mut world, &did_open_uri_message(URI, main)); + let opened_math = dispatch(&mut world, &did_open_uri_message(MATH_URI, math_no_export)); + let main_after_math_open = diagnostic_notification_for_uri(&opened_math, URI); + assert!( + diagnostics_contain_code( + &main_after_math_open, + hir::diag::DiagnosticCode::MODULE_UNKNOWN_IMPORT_ITEM, + ), + "expected main diagnostics to report the genuinely missing export, got {main_after_math_open:#?}" + ); + + let changed_math = dispatch( + &mut world, + &did_change_uri_message(MATH_URI, math_with_export), + ); + let main_after_export = diagnostic_notification_for_uri(&changed_math, URI); + assert!( + !diagnostics_contain_code( + &main_after_export, + hir::diag::DiagnosticCode::MODULE_UNKNOWN_IMPORT_ITEM, + ), + "expected main diagnostics to clear unknown import item, got {main_after_export:#?}" + ); + assert!( + !diagnostics_contain_code( + &main_after_export, + hir::diag::DiagnosticCode::MODULE_NOT_FOUND, + ), + "expected main diagnostics to keep the sibling module resolved, got {main_after_export:#?}" + ); + } + + #[test] + fn hover_and_document_symbol_requests_return_results() { + let mut world = WorldState::new(); + let source = "function main() returns (word) {\n return 42;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let hover = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "hover-1", + "method": "textDocument/hover", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": 9 } + } + }) + .to_string(), + ); + assert_eq!(hover.len(), 1); + let hover_response = parse_message(&hover[0]); + assert_eq!(hover_response["id"], "hover-1"); + assert!( + !hover_response["result"].is_null(), + "expected hover result, got {hover_response:#?}" + ); + + let symbols = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/documentSymbol", + "params": { + "textDocument": { "uri": URI } + } + }) + .to_string(), + ); + assert_eq!(symbols.len(), 1); + let symbol_response = parse_message(&symbols[0]); + assert_eq!(symbol_response["id"], 2); + assert!( + !symbol_response["result"].is_null(), + "expected document symbol result, got {symbol_response:#?}" + ); + } + + #[test] + fn completion_request_returns_items() { + let mut world = WorldState::new(); + let source = "function helper() returns (word) { return 1; }\nfunction main(x: word) returns (word) { return x; }\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + let character = source + .lines() + .nth(1) + .expect("main line") + .find("return x") + .expect("return x") + + "return ".len(); + + let completion = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "completion-1", + "method": "textDocument/completion", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": character } + } + }) + .to_string(), + ); + assert_eq!(completion.len(), 1); + let completion_response = parse_message(&completion[0]); + assert_eq!(completion_response["id"], "completion-1"); + let items = completion_response["result"] + .as_array() + .expect("completion result array"); + assert!( + items.iter().any(|item| item["label"] == "helper"), + "expected helper completion, got {completion_response:#?}" + ); + } + + #[test] + fn references_request_returns_locations() { + let mut world = WorldState::new(); + let source = "function id(x: word) returns (word) {\n return x;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let references = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "refs-1", + "method": "textDocument/references", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": 9 }, + "context": { "includeDeclaration": true } + } + }) + .to_string(), + ); + + assert_eq!(references.len(), 1); + let response = parse_message(&references[0]); + assert_eq!(response["id"], "refs-1"); + let result = response["result"] + .as_array() + .expect("references result array"); + assert_eq!( + result.len(), + 2, + "expected declaration and use references, got {response:#?}" + ); + } + + #[test] + fn signature_help_request_returns_active_parameter() { + let mut world = WorldState::new(); + let source = "function f(a: word, b: word) returns (word) {\n return a;\n}\n\nfunction main() returns (word) {\n return f(1, 2);\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let help = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "signature-1", + "method": "textDocument/signatureHelp", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 5, "character": 13 } + } + }) + .to_string(), + ); + assert_eq!(help.len(), 1); + let response = parse_message(&help[0]); + + assert_eq!(response["id"], "signature-1"); + assert_eq!(response["result"]["activeSignature"], 0); + assert_eq!(response["result"]["activeParameter"], 1); + let label = response["result"]["signatures"][0]["label"] + .as_str() + .expect("signature label"); + assert!( + label.contains("f(") && label.contains("a: word") && label.contains("b: word"), + "expected rendered signature label, got {label}" + ); + } + + #[test] + fn semantic_tokens_full_request_returns_tokens() { + let mut world = WorldState::new(); + let source = "function main(x: word) returns (word) {\n return x;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let semantic_tokens = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "tokens-1", + "method": "textDocument/semanticTokens/full", + "params": { + "textDocument": { "uri": URI } + } + }) + .to_string(), + ); + assert_eq!(semantic_tokens.len(), 1); + let response = parse_message(&semantic_tokens[0]); + assert_eq!(response["id"], "tokens-1"); + let data = response["result"]["data"] + .as_array() + .expect("semantic token data array"); + assert!( + data.len() >= 5 && data.len().is_multiple_of(5), + "expected packed semantic token data, got {response:#?}" + ); + assert_eq!(data[0], 0); + assert_eq!(data[1], source.find("main").expect("main") as u32); + } + + #[test] + fn inlay_hint_request_returns_results() { + let mut world = WorldState::new(); + let source = "function main() returns (word) {\n let x = 42;\n return x;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let inlay_hints = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "inlay-1", + "method": "textDocument/inlayHint", + "params": { + "textDocument": { "uri": URI }, + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 4, "character": 0 } + } + } + }) + .to_string(), + ); + assert_eq!(inlay_hints.len(), 1); + let response = parse_message(&inlay_hints[0]); + assert_eq!(response["id"], "inlay-1"); + let hints = response["result"].as_array().expect("hint result array"); + assert_eq!(hints.len(), 1, "expected one hint, got {response:#?}"); + assert_eq!(hints[0]["label"], ": word"); + } + + #[test] + fn workspace_symbol_request_returns_matching_symbols() { + let mut world = WorldState::new(); + let source = "function target() returns (word) {\n return 42;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let symbols = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "symbols-1", + "method": "workspace/symbol", + "params": { + "query": "target" + } + }) + .to_string(), + ); + assert_eq!(symbols.len(), 1); + let response = parse_message(&symbols[0]); + assert_eq!(response["id"], "symbols-1"); + let result = response["result"] + .as_array() + .expect("workspace symbol result array"); + assert_eq!(result.len(), 1); + assert_eq!(result[0]["name"], "target"); + assert_eq!(result[0]["location"]["uri"], URI); + } + + #[test] + fn code_action_formatting_folding_and_selection_requests_return_results() { + let mut world = WorldState::new(); + let source = "function value() returns (word) { return 1; }\nfunction main() returns (word) {\n/* 😀 */ return vaue();\n}\n"; + let opened = dispatch(&mut world, &did_open_message(source)); + let notification = diagnostic_notification_for_uri(&opened, URI); + let diagnostic = notification["params"]["diagnostics"] + .as_array() + .and_then(|diagnostics| diagnostics.first()) + .cloned() + .expect("published diagnostic"); + let range = diagnostic["range"].clone(); + + let actions = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "actions-1", + "method": "textDocument/codeAction", + "params": { + "textDocument": { "uri": URI }, + "range": range, + "context": { "diagnostics": [diagnostic], "only": ["quickfix"] } + } + }) + .to_string(), + ); + let actions = parse_message(&actions[0]); + let actions = actions["result"].as_array().expect("code action array"); + assert_eq!(actions.len(), 1); + let action = &actions[0]; + assert_eq!(action["title"], "Replace with `value`"); + assert_eq!(action["kind"], "quickfix"); + assert_eq!(action["isPreferred"], false); + assert_eq!( + action["diagnostics"] + .as_array() + .and_then(|diagnostics| diagnostics.first()), + Some(&diagnostic) + ); + let edit = &action["edit"]["changes"][URI][0]; + assert_eq!(edit["newText"], "value"); + assert_eq!( + edit["range"], + serde_json::json!({ + "start": { "line": 2, "character": 16 }, + "end": { "line": 2, "character": 20 } + }) + ); + + let formatting = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "format-1", + "method": "textDocument/formatting", + "params": { + "textDocument": { "uri": URI }, + "options": { "tabSize": 2, "insertSpaces": true } + } + }) + .to_string(), + ); + let formatting = parse_message(&formatting[0]); + assert_eq!( + formatting["result"].as_array().expect("format edits").len(), + 1 + ); + + let folding = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "folding-1", + "method": "textDocument/foldingRange", + "params": { "textDocument": { "uri": URI } } + }) + .to_string(), + ); + let folding = parse_message(&folding[0]); + assert!( + !folding["result"] + .as_array() + .expect("folding ranges") + .is_empty() + ); + + let selection = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "selection-1", + "method": "textDocument/selectionRange", + "params": { + "textDocument": { "uri": URI }, + "positions": [{ "line": 2, "character": 9 }] + } + }) + .to_string(), + ); + let selection = parse_message(&selection[0]); + let ranges = selection["result"].as_array().expect("selection ranges"); + assert_eq!(ranges.len(), 1); + assert!(ranges[0]["parent"].is_object()); + } + + #[test] + fn missing_import_code_action_round_trips_over_wasm_dispatch() { + let mut world = WorldState::new(); + let provider = "function value() returns (word) { return 1; }\n\nexport { value };\n"; + let main = "function main() returns (word) { return value(); }\n"; + + let _ = dispatch(&mut world, &did_open_uri_message(MATH_URI, provider)); + let opened = dispatch(&mut world, &did_open_uri_message(URI, main)); + let notification = diagnostic_notification_for_uri(&opened, URI); + let diagnostic = notification["params"]["diagnostics"] + .as_array() + .and_then(|diagnostics| { + diagnostics.iter().find(|diagnostic| { + diagnostic["code"] == hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME + }) + }) + .cloned() + .expect("undefined-name diagnostic"); + let range = diagnostic["range"].clone(); + + let actions = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "missing-import-1", + "method": "textDocument/codeAction", + "params": { + "textDocument": { "uri": URI }, + "range": range, + "context": { "diagnostics": [diagnostic], "only": ["quickfix"] } + } + }) + .to_string(), + ); + let response = parse_message(&actions[0]); + let actions = response["result"] + .as_array() + .expect("code action result array"); + assert_eq!(actions.len(), 1, "expected one auto-import: {response:#?}"); + let action = &actions[0]; + assert_eq!(action["title"], "Import `value` from `lib.math`"); + assert_eq!(action["kind"], "quickfix"); + assert_eq!(action["isPreferred"], true); + assert_eq!( + action["edit"]["changes"][URI][0], + serde_json::json!({ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 } + }, + "newText": "import {value} from lib.math;\n" + }) + ); + } + + #[test] + fn qualified_import_code_actions_round_trip_over_wasm_dispatch() { + let cases = [ + ( + "enum Option { None, Some(word) }\nexport { Option(*) };\n", + "function main() returns (word) { let option = Option.Some(1); return 1; }\n", + "Import `Option` from `lib.math`", + "import {Option} from lib.math;\n", + ), + ( + "function value() returns (word) { return 1; }\nexport { value };\n", + "function main() returns (word) { return math.value(); }\n", + "Import module `math` from `lib.math`", + "import * as math from lib.math;\n", + ), + ]; + + for (provider, main, expected_title, expected_edit) in cases { + let mut world = WorldState::new(); + let _ = dispatch(&mut world, &did_open_uri_message(MATH_URI, provider)); + let opened = dispatch(&mut world, &did_open_uri_message(URI, main)); + let notification = diagnostic_notification_for_uri(&opened, URI); + let diagnostic = notification["params"]["diagnostics"] + .as_array() + .and_then(|diagnostics| { + diagnostics.iter().find(|diagnostic| { + diagnostic["code"] == hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME + }) + }) + .cloned() + .expect("qualified undefined-name diagnostic"); + + let response = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "qualified-missing-import", + "method": "textDocument/codeAction", + "params": { + "textDocument": { "uri": URI }, + "range": diagnostic["range"], + "context": { "diagnostics": [diagnostic], "only": ["quickfix"] } + } + }) + .to_string(), + ); + let response = parse_message(&response[0]); + let actions = response["result"] + .as_array() + .expect("code action result array"); + + assert_eq!(actions.len(), 1, "expected one action: {response:#?}"); + assert_eq!(actions[0]["title"], expected_title); + assert_eq!(actions[0]["isPreferred"], true); + assert_eq!( + actions[0]["edit"]["changes"][URI][0]["newText"], + expected_edit + ); + } + } + + #[test] + fn standard_library_missing_import_round_trips_over_wasm_dispatch() { + let mut world = WorldState::new(); + let source = "function main() returns (word) { assert(true); return 1; }\n"; + let opened = dispatch(&mut world, &did_open_message(source)); + let notification = diagnostic_notification_for_uri(&opened, URI); + let diagnostic = notification["params"]["diagnostics"] + .as_array() + .and_then(|diagnostics| { + diagnostics.iter().find(|diagnostic| { + diagnostic["code"] == hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME + }) + }) + .cloned() + .expect("undefined-name diagnostic"); + + let actions = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "std-missing-import-1", + "method": "textDocument/codeAction", + "params": { + "textDocument": { "uri": URI }, + "range": diagnostic["range"], + "context": { "diagnostics": [diagnostic], "only": ["quickfix"] } + } + }) + .to_string(), + ); + let response = parse_message(&actions[0]); + let actions = response["result"] + .as_array() + .expect("code action result array"); + assert_eq!( + actions.len(), + 1, + "expected one std auto-import: {response:#?}" + ); + assert_eq!(actions[0]["title"], "Import `assert` from `std`"); + assert_eq!( + actions[0]["edit"]["changes"][URI][0]["newText"], + "import {assert} from std;\n" + ); + } + + #[test] + fn closing_untitled_document_removes_it_from_workspace_symbols() { + let mut world = WorldState::new(); + let uri = "untitled:Untitled-1"; + let source = "function ghost() returns (word) { return 42; }\n"; + let _ = dispatch(&mut world, &did_open_uri_message(uri, source)); + + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didClose", + "params": { "textDocument": { "uri": uri } } + }) + .to_string(), + ); + let symbols = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "symbols-after-close", + "method": "workspace/symbol", + "params": { "query": "ghost" } + }) + .to_string(), + ); + + let response = parse_message(&symbols[0]); + assert!( + response["result"] + .as_array() + .expect("symbol array") + .is_empty() + ); + } + + #[test] + fn closing_workspace_document_removes_it_from_workspace_symbols() { + let mut world = WorldState::new(); + let uri = "file:///main/ghost.solc"; + let source = "function ghost() returns (word) { return 42; }\n"; + let _ = dispatch(&mut world, &did_open_uri_message(uri, source)); + + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didClose", + "params": { "textDocument": { "uri": uri } } + }) + .to_string(), + ); + let symbols = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "workspace-symbols-after-close", + "method": "workspace/symbol", + "params": { "query": "ghost" } + }) + .to_string(), + ); + + let response = parse_message(&symbols[0]); + assert_eq!(response["result"], serde_json::json!([])); + assert!( + !world + .workspace_document_uris() + .contains(&lsp_types::Url::parse(uri).expect("workspace uri")) + ); + } + + #[test] + fn closing_file_detached_from_removed_workspace_discards_it() { + let mut world = WorldState::new(); + let root = "file:///main/"; + let uri = "file:///main/ghost.solc"; + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "capabilities": {}, + "workspaceFolders": [{ "uri": root, "name": "left" }] + } + }) + .to_string(), + ); + let _ = dispatch( + &mut world, + &did_open_uri_message(uri, "function ghost() returns (word) { return 42; }\n"), + ); + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWorkspaceFolders", + "params": { + "event": { + "added": [], + "removed": [{ "uri": root, "name": "left" }] + } + } + }) + .to_string(), + ); + let _ = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didClose", + "params": { "textDocument": { "uri": uri } } + }) + .to_string(), + ); + + assert!( + world + .line_index(&lsp_types::Url::parse(uri).expect("uri")) + .is_none() + ); + let symbols = crate::workspace_symbols::handle_workspace_symbol(&world, "ghost") + .expect("workspace symbols"); + assert!(symbols.is_empty()); + } + + #[test] + fn document_highlight_request_returns_highlights() { + let mut world = WorldState::new(); + let source = "function id(x: word) returns (word) {\n return x;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let highlights = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "highlights-1", + "method": "textDocument/documentHighlight", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": 9 } + } + }) + .to_string(), + ); + + assert_eq!(highlights.len(), 1); + let response = parse_message(&highlights[0]); + assert_eq!(response["id"], "highlights-1"); + let result = response["result"] + .as_array() + .expect("document highlight result array"); + assert_eq!( + result.len(), + 2, + "expected declaration and use highlights, got {response:#?}" + ); + assert!( + result.iter().all(|highlight| highlight["kind"] == 1), + "expected text highlight kinds, got {response:#?}" + ); + } + + #[test] + fn rename_requests_return_workspace_edit_and_prepare_range() { + let mut world = WorldState::new(); + let source = "function id(x: word) returns (word) {\n let y = x;\n return x;\n}\n"; + let outgoing = dispatch(&mut world, &did_open_message(source)); + assert_eq!(outgoing.len(), 1); + + let prepare = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "prepare-rename-1", + "method": "textDocument/prepareRename", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": 10 } + } + }) + .to_string(), + ); + assert_eq!(prepare.len(), 1); + let prepare_response = parse_message(&prepare[0]); + assert_eq!(prepare_response["id"], "prepare-rename-1"); + assert_eq!(prepare_response["result"]["start"]["line"], 1); + assert_eq!(prepare_response["result"]["start"]["character"], 10); + assert_eq!(prepare_response["result"]["end"]["line"], 1); + assert_eq!(prepare_response["result"]["end"]["character"], 11); + + let rename = dispatch( + &mut world, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": "rename-1", + "method": "textDocument/rename", + "params": { + "textDocument": { "uri": URI }, + "position": { "line": 1, "character": 10 }, + "newName": "renamed" + } + }) + .to_string(), + ); + + assert_eq!(rename.len(), 1); + let response = parse_message(&rename[0]); + assert_eq!(response["id"], "rename-1"); + let edits = response["result"]["changes"][URI] + .as_array() + .expect("rename edits array"); + assert_eq!(edits.len(), 3, "expected declaration and two uses"); + assert!(edits.iter().all(|edit| edit["newText"] == "renamed")); + assert_eq!(edits[0]["range"]["start"]["line"], 0); + assert_eq!(edits[0]["range"]["start"]["character"], 12); + assert_eq!(edits[1]["range"]["start"]["line"], 1); + assert_eq!(edits[1]["range"]["start"]["character"], 10); + assert_eq!(edits[2]["range"]["start"]["line"], 2); + assert_eq!(edits[2]["range"]["start"]["character"], 9); + } + + fn did_open_message(source: &str) -> String { + did_open_uri_message(URI, source) + } + + fn did_open_uri_message(uri: &str, source: &str) -> String { + serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": uri, + "languageId": "solcore", + "version": 1, + "text": source + } + } + }) + .to_string() + } + + fn did_change_uri_message(uri: &str, source: &str) -> String { + serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": uri, + "version": 2, + }, + "contentChanges": [ + { + "text": source + } + ] + } + }) + .to_string() + } + + fn diagnostic_notification_for_uri(outgoing: &[String], uri: &str) -> Value { + outgoing + .iter() + .map(|message| parse_message(message)) + .find(|message| { + message["method"] == "textDocument/publishDiagnostics" + && message["params"]["uri"] == uri + }) + .unwrap_or_else(|| panic!("expected diagnostics for {uri}, got {outgoing:#?}")) + } + + fn diagnostics_contain_code(notification: &Value, code: &str) -> bool { + notification["params"]["diagnostics"] + .as_array() + .expect("diagnostics array") + .iter() + .any(|diagnostic| diagnostic["code"] == code) + } + + fn parse_message(message: &str) -> Value { + serde_json::from_str(message).expect("valid outgoing JSON-RPC message") + } +} diff --git a/crates/lsp/src/workspace_symbols.rs b/crates/lsp/src/workspace_symbols.rs new file mode 100644 index 00000000..f9ba4900 --- /dev/null +++ b/crates/lsp/src/workspace_symbols.rs @@ -0,0 +1,445 @@ +//! Workspace symbol support over the wasm-clean LSP core. + +use std::cmp::Ordering; + +use hir::{ + ast::item::{ContractItem, FieldDef, FuncKind, InstanceDef, Item}, + span::{Span, Spanned}, +}; +use lsp_types::{Location, Range, SymbolInformation, SymbolKind, Url}; + +use crate::state::WorldState; + +const MAX_WORKSPACE_SYMBOLS: usize = 256; + +/// Computes flat workspace symbols for every source document loaded in the +/// workspace. +pub fn handle_workspace_symbol(world: &WorldState, query: &str) -> Option> { + let db = world.db(); + let query = query.to_lowercase(); + let mut symbols = Vec::new(); + + for uri in world.workspace_document_uris() { + let Some(path) = world.vfs_path_for_uri(&uri) else { + continue; + }; + let Some(file) = db.source_file(&path) else { + continue; + }; + let Some(line_index) = world.line_index(&uri) else { + continue; + }; + let module = parser::parse_file_to_hir(db, file).module(db); + + for item in module.items(db) { + collect_item_symbols(db, line_index, &uri, *item, &mut symbols); + } + } + + if !query.is_empty() { + symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query)); + } + symbols.sort_by(compare_symbols); + // Keep project-wide responses bounded for large preloaded workspaces. + symbols.truncate(MAX_WORKSPACE_SYMBOLS); + + Some(symbols) +} + +fn collect_item_symbols<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + uri: &Url, + item: Item<'db>, + symbols: &mut Vec, +) { + match item { + Item::FunctionDef(function) => symbols.push(symbol_information( + db, + line_index, + uri, + function.sig(db).name.atom().text(db).to_owned(), + SymbolKind::FUNCTION, + function.sig(db).name.span(db), + None, + )), + Item::TypeAlias(alias) => symbols.push(symbol_information( + db, + line_index, + uri, + alias.name_elem(db).atom().text(db).to_owned(), + SymbolKind::CLASS, + alias.name_elem(db).span(db), + None, + )), + Item::AdtDef(adt) => symbols.push(symbol_information( + db, + line_index, + uri, + adt.name_elem(db).atom().text(db).to_owned(), + SymbolKind::ENUM, + adt.name_elem(db).span(db), + None, + )), + Item::ClassDef(class) => { + let name = class.head(db).kind(db).class; + symbols.push(symbol_information( + db, + line_index, + uri, + name.atom().text(db).to_owned(), + SymbolKind::INTERFACE, + name.span(db), + None, + )); + } + Item::InstanceDef(instance) => { + symbols.push(instance_symbol(db, line_index, uri, instance)); + } + Item::ContractDef(contract) => { + let contract_name = contract.name_elem(db).atom().text(db).to_owned(); + symbols.push(symbol_information( + db, + line_index, + uri, + contract_name.clone(), + contract_symbol_kind(contract.kind(db)), + contract.name_elem(db).span(db), + None, + )); + + for field in contract.fields(db) { + symbols.push(field_symbol( + db, + line_index, + uri, + field, + contract_name.clone(), + )); + } + for item in contract.items(db) { + collect_contract_item_symbols( + db, + line_index, + uri, + *item, + contract_name.clone(), + symbols, + ); + } + } + Item::Import(_) | Item::Export(_) | Item::Pragma(_) | Item::Error { .. } => {} + } +} + +fn contract_symbol_kind(kind: hir::ast::item::ContractKind) -> SymbolKind { + match kind { + hir::ast::item::ContractKind::Contract => SymbolKind::CLASS, + hir::ast::item::ContractKind::Interface => SymbolKind::INTERFACE, + hir::ast::item::ContractKind::Library => SymbolKind::MODULE, + } +} + +fn collect_contract_item_symbols<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + uri: &Url, + item: ContractItem<'db>, + contract_name: String, + symbols: &mut Vec, +) { + match item { + ContractItem::FunctionDef(function) => { + let kind = match function.kind(db) { + FuncKind::Constructor => SymbolKind::CONSTRUCTOR, + FuncKind::Function | FuncKind::Fallback => SymbolKind::METHOD, + }; + symbols.push(symbol_information( + db, + line_index, + uri, + function.sig(db).name.atom().text(db).to_owned(), + kind, + function.sig(db).name.span(db), + Some(contract_name), + )); + } + ContractItem::TypeAlias(alias) => symbols.push(symbol_information( + db, + line_index, + uri, + alias.name_elem(db).atom().text(db).to_owned(), + SymbolKind::CLASS, + alias.name_elem(db).span(db), + Some(contract_name), + )), + ContractItem::AdtDef(adt) => symbols.push(symbol_information( + db, + line_index, + uri, + adt.name_elem(db).atom().text(db).to_owned(), + SymbolKind::ENUM, + adt.name_elem(db).span(db), + Some(contract_name), + )), + ContractItem::Error { .. } => {} + } +} + +fn field_symbol<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + uri: &Url, + field: &FieldDef<'db>, + contract_name: String, +) -> SymbolInformation { + symbol_information( + db, + line_index, + uri, + field.name().atom().text(db).to_owned(), + SymbolKind::FIELD, + field.name().span(db), + Some(contract_name), + ) +} + +fn instance_symbol<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + uri: &Url, + instance: InstanceDef<'db>, +) -> SymbolInformation { + let head = instance.head(db); + let class = head.kind(db).class; + symbol_information( + db, + line_index, + uri, + format!("impl {}", class.atom().text(db)), + SymbolKind::OBJECT, + class.span(db), + None, + ) +} + +fn symbol_information<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + uri: &Url, + name: String, + kind: SymbolKind, + selection_span: Span<'db>, + container_name: Option, +) -> SymbolInformation { + #[allow(deprecated)] + let symbol = SymbolInformation { + name, + kind, + tags: None, + deprecated: None, + location: Location::new(uri.clone(), lsp_range(db, line_index, selection_span)), + container_name, + }; + symbol +} + +fn lsp_range<'db>( + db: &'db dyn parser::Db, + line_index: &crate::LineIndexExt, + span: Span<'db>, +) -> Range { + let absolute = span.resolve_to_absolute(db); + line_index.range(absolute.start().as_u32(), absolute.end().as_u32()) +} + +fn compare_symbols(left: &SymbolInformation, right: &SymbolInformation) -> Ordering { + left.name + .cmp(&right.name) + .then_with(|| left.location.uri.as_str().cmp(right.location.uri.as_str())) + .then_with(|| compare_ranges(&left.location.range, &right.location.range)) +} + +fn compare_ranges(left: &Range, right: &Range) -> Ordering { + left.start + .line + .cmp(&right.start.line) + .then(left.start.character.cmp(&right.start.character)) + .then(left.end.line.cmp(&right.end.line)) + .then(left.end.character.cmp(&right.end.character)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn world_with_main(source: &str) -> (WorldState, Url) { + let mut world = WorldState::new(); + let uri = Url::parse("file:///main/main.solc").expect("uri"); + assert!(world.open_document(uri.clone(), source.to_owned())); + (world, uri) + } + + #[test] + fn query_returns_matching_functions_from_each_open_document() { + let main_source = "function target_main() returns (word) {\n return 1;\n}\n"; + let util_source = "function target_util() returns (word) {\n return 2;\n}\n"; + let (mut world, main_uri) = world_with_main(main_source); + let util_uri = Url::parse("file:///main/util.solc").expect("uri"); + assert!(world.open_document(util_uri.clone(), util_source.to_owned())); + + let symbols = handle_workspace_symbol(&world, "TARGET").expect("workspace symbols"); + assert_eq!(symbols.len(), 2); + assert_symbol_at( + &world, + &symbols[0], + "target_main", + SymbolKind::FUNCTION, + &main_uri, + main_source, + ); + assert_symbol_at( + &world, + &symbols[1], + "target_util", + SymbolKind::FUNCTION, + &util_uri, + util_source, + ); + } + + #[test] + fn query_includes_preloaded_but_unopened_workspace_documents() { + let mut world = WorldState::new(); + let root_path = std::env::temp_dir().join("solcore-lsp-symbol-project"); + let root = Url::from_directory_path(&root_path).expect("root uri"); + let main_uri = Url::from_file_path(root_path.join("main.solc")).expect("main uri"); + let util_uri = Url::from_file_path(root_path.join("util.solc")).expect("util uri"); + assert_eq!( + world.load_workspace_documents( + root, + [ + ( + main_uri, + "function main_symbol() returns (word) { return 1; }\n".to_owned() + ), + ( + util_uri.clone(), + "function unopened_symbol() returns (word) { return 2; }\n".to_owned() + ), + ] + ), + 2 + ); + + let symbols = + handle_workspace_symbol(&world, "unopened").expect("workspace symbol response"); + + assert_eq!(symbols.len(), 1); + assert_eq!(symbols[0].name, "unopened_symbol"); + assert_eq!(symbols[0].location.uri, util_uri); + assert!(world.open_document_uris().is_empty()); + } + + #[test] + fn empty_query_returns_top_level_symbols_and_non_matching_query_is_empty() { + let source = "\ +function alpha() returns (word) { + return 1; +} + +alias Alias = word; + +enum Choice { One, Two } + +contract Vault {} + +interface Reader { + function read(key: word) external view returns (word); +} + +library Helpers { + function identity(value: word) internal pure returns (word) { return value; } +} +"; + let (world, uri) = world_with_main(source); + + let symbols = handle_workspace_symbol(&world, "").expect("workspace symbols"); + let names = symbols + .iter() + .map(|symbol| symbol.name.as_str()) + .collect::>(); + assert_eq!( + names, + [ + "Alias", "Choice", "Helpers", "Reader", "Vault", "alpha", "identity", "read" + ] + ); + assert!(symbols.iter().all(|symbol| symbol.location.uri == uri)); + assert_eq!( + symbols.iter().map(|symbol| symbol.kind).collect::>(), + [ + SymbolKind::CLASS, + SymbolKind::ENUM, + SymbolKind::MODULE, + SymbolKind::INTERFACE, + SymbolKind::CLASS, + SymbolKind::FUNCTION, + SymbolKind::METHOD, + SymbolKind::METHOD, + ] + ); + + let non_matching = + handle_workspace_symbol(&world, "does-not-exist").expect("workspace symbols"); + assert!(non_matching.is_empty()); + } + + #[test] + fn contract_member_symbols_keep_container_name() { + let source = "\ +contract Vault { + balance: word; + function read() returns (word) { + return balance; + } +} +"; + let (world, uri) = world_with_main(source); + + let field = handle_workspace_symbol(&world, "balance") + .expect("workspace symbols") + .into_iter() + .find(|symbol| symbol.name == "balance") + .expect("balance symbol"); + assert_symbol_at(&world, &field, "balance", SymbolKind::FIELD, &uri, source); + assert_eq!(field.container_name, Some("Vault".to_owned())); + + let method = handle_workspace_symbol(&world, "read") + .expect("workspace symbols") + .into_iter() + .find(|symbol| symbol.name == "read") + .expect("read symbol"); + assert_symbol_at(&world, &method, "read", SymbolKind::METHOD, &uri, source); + assert_eq!(method.container_name, Some("Vault".to_owned())); + } + + fn assert_symbol_at( + world: &WorldState, + symbol: &SymbolInformation, + name: &str, + kind: SymbolKind, + uri: &Url, + source: &str, + ) { + assert_eq!(symbol.name, name); + assert_eq!(symbol.kind, kind); + assert_eq!(symbol.location.uri, *uri); + let start = source.find(name).expect("symbol name") as u32; + let end = start + name.len() as u32; + assert_eq!( + symbol.location.range, + world.line_index(uri).expect("line index").range(start, end) + ); + } +} diff --git a/crates/lsp/tests/stdio_smoke.rs b/crates/lsp/tests/stdio_smoke.rs new file mode 100644 index 00000000..d8016231 --- /dev/null +++ b/crates/lsp/tests/stdio_smoke.rs @@ -0,0 +1,565 @@ +#![cfg(feature = "native")] + +use std::{ + fs, + io::{self, BufRead, BufReader, Read, Write}, + path::PathBuf, + process::{Child, ChildStdin, Command, Stdio}, + sync::mpsc, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use lsp_types::Url; +use serde_json::{Value, json}; + +const MAIN_SOURCE: &str = "\ +import {double} from math; + +function f() returns (word) { + return double(true); +} +"; +const MATH_SOURCE: &str = "\ +function double(x: word) returns (word) { + return x; +} + +export { double }; +"; +const SECONDARY_SOURCE: &str = "function secondaryValue() returns (word) { return 2; }\n"; + +struct TestWorkspace { + root: PathBuf, + root_uri: Url, + main_uri: Url, + math_uri: Url, + secondary_root: PathBuf, + secondary_root_uri: Url, + secondary_uri: Url, +} + +impl TestWorkspace { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "solcore-lsp-stdio-smoke-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&root).expect("create test workspace"); + let main = root.join("main.solc"); + let math = root.join("math.solc"); + fs::write(&main, MAIN_SOURCE).expect("write main source"); + fs::write(&math, MATH_SOURCE).expect("write math source"); + let secondary_root = std::env::temp_dir().join(format!( + "solcore-lsp-stdio-smoke-secondary-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&secondary_root).expect("create secondary workspace"); + let secondary = secondary_root.join("secondary.solc"); + fs::write(&secondary, SECONDARY_SOURCE).expect("write secondary source"); + + Self { + root_uri: Url::from_directory_path(&root).expect("workspace root URI"), + main_uri: Url::from_file_path(main).expect("main URI"), + math_uri: Url::from_file_path(math).expect("math URI"), + secondary_root_uri: Url::from_directory_path(&secondary_root) + .expect("secondary workspace root URI"), + secondary_uri: Url::from_file_path(secondary).expect("secondary URI"), + secondary_root, + root, + } + } +} + +impl Drop for TestWorkspace { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + let _ = fs::remove_dir_all(&self.secondary_root); + } +} + +#[test] +fn native_stdio_publishes_diagnostics() { + let workspace = TestWorkspace::new(); + let mut child = Command::new(env!("CARGO_BIN_EXE_solcore-lsp")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn solcore-lsp"); + + let mut stdin = child.stdin.take().expect("child stdin"); + let stdout = child.stdout.take().expect("child stdout"); + let stderr = child.stderr.take().expect("child stderr"); + + let (messages_tx, messages_rx) = mpsc::channel(); + let reader = thread::spawn(move || { + let mut stdout = BufReader::new(stdout); + while let Ok(message) = read_message(&mut stdout) { + if messages_tx.send(message).is_err() { + break; + } + } + }); + + let (stderr_tx, stderr_rx) = mpsc::channel(); + let stderr_reader = thread::spawn(move || { + let mut stderr = BufReader::new(stderr); + let mut output = String::new(); + let _ = stderr.read_to_string(&mut output); + let _ = stderr_tx.send(output); + }); + + let result = run_lsp_smoke(&mut stdin, &messages_rx, &workspace); + let shutdown_result = shutdown_child(&mut child, stdin, &messages_rx); + + let _ = reader.join(); + let _ = stderr_reader.join(); + + if let Err(error) = result.and(shutdown_result) { + let stderr = stderr_rx.try_recv().unwrap_or_default(); + panic!("{error}\nchild stderr:\n{stderr}"); + } +} + +fn run_lsp_smoke( + stdin: &mut ChildStdin, + messages_rx: &mpsc::Receiver, + workspace: &TestWorkspace, +) -> Result<(), String> { + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "rootUri": workspace.root_uri, + "workspaceFolders": [ + { "uri": workspace.root_uri, "name": "primary" }, + { "uri": workspace.secondary_root_uri, "name": "secondary" } + ], + "capabilities": {} + } + }), + )?; + + let initialize = recv_until(messages_rx, |message| { + message.get("id").and_then(Value::as_i64) == Some(1) + })?; + if initialize.pointer("/result/capabilities").is_none() { + return Err(format!( + "initialize response did not contain capabilities: {initialize}" + )); + } + + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "method": "initialized", + "params": {} + }), + )?; + + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": workspace.main_uri, + "languageId": "solcore", + "version": 1, + "text": MAIN_SOURCE + } + } + }), + )?; + + let diagnostics = recv_until(messages_rx, |message| { + message.get("method").and_then(Value::as_str) == Some("textDocument/publishDiagnostics") + && message.pointer("/params/uri").and_then(Value::as_str) + == Some(workspace.main_uri.as_str()) + && message + .pointer("/params/diagnostics") + .and_then(Value::as_array) + .is_some_and(|diagnostics| !diagnostics.is_empty()) + })?; + + let has_error = diagnostics + .pointer("/params/diagnostics") + .and_then(Value::as_array) + .is_some_and(|diagnostics| { + diagnostics + .iter() + .any(|diagnostic| diagnostic.get("severity").and_then(Value::as_u64) == Some(1)) + }); + + if !has_error { + return Err(format!("expected an error diagnostic, got: {diagnostics}")); + } + + let published_diagnostic = diagnostics + .pointer("/params/diagnostics") + .and_then(Value::as_array) + .and_then(|diagnostics| diagnostics.first()) + .cloned() + .ok_or_else(|| format!("missing published diagnostic: {diagnostics}"))?; + let diagnostic_range = published_diagnostic + .get("range") + .cloned() + .ok_or_else(|| format!("published diagnostic had no range: {published_diagnostic}"))?; + let code_actions = request_result( + stdin, + messages_rx, + 3, + "textDocument/codeAction", + json!({ + "textDocument": { "uri": workspace.main_uri }, + "range": diagnostic_range, + "context": { "diagnostics": [published_diagnostic] } + }), + )?; + if !code_actions.is_array() { + return Err(format!( + "code action result was not an array: {code_actions}" + )); + } + + let formatting = request_result( + stdin, + messages_rx, + 4, + "textDocument/formatting", + json!({ + "textDocument": { "uri": workspace.main_uri }, + "options": { "tabSize": 2, "insertSpaces": true } + }), + )?; + if !formatting.is_array() { + return Err(format!("formatting result was not an array: {formatting}")); + } + + let folding = request_result( + stdin, + messages_rx, + 5, + "textDocument/foldingRange", + json!({ "textDocument": { "uri": workspace.main_uri } }), + )?; + if folding.as_array().is_none_or(|ranges| ranges.is_empty()) { + return Err(format!("expected folding ranges, got: {folding}")); + } + + let selection = request_result( + stdin, + messages_rx, + 6, + "textDocument/selectionRange", + json!({ + "textDocument": { "uri": workspace.main_uri }, + "positions": [{ "line": 3, "character": 10 }] + }), + )?; + if selection.as_array().is_none_or(|ranges| ranges.len() != 1) { + return Err(format!("expected one selection range, got: {selection}")); + } + + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/definition", + "params": { + "textDocument": { "uri": workspace.main_uri }, + "position": { "line": 3, "character": 10 } + } + }), + )?; + + let definition = recv_until(messages_rx, |message| { + message.get("id").and_then(Value::as_i64) == Some(2) + })?; + if definition.pointer("/result/uri").and_then(Value::as_str) + != Some(workspace.math_uri.as_str()) + { + return Err(format!( + "expected definition in unopened sibling {}, got: {definition}", + workspace.math_uri + )); + } + + assert_workspace_symbol( + stdin, + messages_rx, + 10, + "secondaryValue", + Some(&workspace.secondary_uri), + )?; + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWorkspaceFolders", + "params": { + "event": { + "added": [], + "removed": [{ + "uri": workspace.secondary_root_uri, + "name": "secondary" + }] + } + } + }), + )?; + assert_workspace_symbol(stdin, messages_rx, 11, "secondaryValue", None)?; + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWorkspaceFolders", + "params": { + "event": { + "added": [{ + "uri": workspace.secondary_root_uri, + "name": "secondary" + }], + "removed": [] + } + } + }), + )?; + assert_workspace_symbol( + stdin, + messages_rx, + 12, + "secondaryValue", + Some(&workspace.secondary_uri), + )?; + + fs::remove_file(workspace.root.join("math.solc")) + .map_err(|error| format!("failed to remove watched math.solc: {error}"))?; + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "method": "workspace/didChangeWatchedFiles", + "params": { + "changes": [{ + "uri": workspace.math_uri, + "type": 3 + }] + } + }), + )?; + let deleted_import_diagnostics = recv_until(messages_rx, |message| { + message.get("method").and_then(Value::as_str) == Some("textDocument/publishDiagnostics") + && message.pointer("/params/uri").and_then(Value::as_str) + == Some(workspace.main_uri.as_str()) + && message + .pointer("/params/diagnostics") + .and_then(Value::as_array) + .is_some_and(|diagnostics| { + diagnostics.iter().any(|diagnostic| { + diagnostic + .get("message") + .and_then(Value::as_str) + .is_some_and(|message| message.contains("file not found")) + }) + }) + })?; + if deleted_import_diagnostics + .pointer("/params/diagnostics") + .and_then(Value::as_array) + .is_none() + { + return Err(format!( + "expected diagnostics after deleting watched import: {deleted_import_diagnostics}" + )); + } + + Ok(()) +} + +fn assert_workspace_symbol( + stdin: &mut ChildStdin, + messages_rx: &mpsc::Receiver, + id: i64, + query: &str, + expected_uri: Option<&Url>, +) -> Result<(), String> { + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "id": id, + "method": "workspace/symbol", + "params": { "query": query } + }), + )?; + let response = recv_until(messages_rx, |message| { + message.get("id").and_then(Value::as_i64) == Some(id) + })?; + let symbols = response + .get("result") + .and_then(Value::as_array) + .ok_or_else(|| format!("workspace symbol response was not an array: {response}"))?; + let found = symbols.iter().any(|symbol| { + symbol.get("name").and_then(Value::as_str) == Some(query) + && expected_uri.is_some_and(|uri| { + symbol.pointer("/location/uri").and_then(Value::as_str) == Some(uri.as_str()) + }) + }); + if expected_uri.is_some() != found { + return Err(format!( + "unexpected workspace symbol result for {query:?}, expected URI {expected_uri:?}: {response}" + )); + } + if expected_uri.is_none() && !symbols.is_empty() { + return Err(format!( + "expected no workspace symbols for {query:?}, got: {response}" + )); + } + Ok(()) +} + +fn request_result( + stdin: &mut ChildStdin, + messages_rx: &mpsc::Receiver, + id: i64, + method: &str, + params: Value, +) -> Result { + send_message( + stdin, + &json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params + }), + )?; + let response = recv_until(messages_rx, |message| { + message.get("id").and_then(Value::as_i64) == Some(id) + })?; + response + .get("result") + .cloned() + .ok_or_else(|| format!("{method} response had no result: {response}")) +} + +fn shutdown_child( + child: &mut Child, + mut stdin: ChildStdin, + messages_rx: &mpsc::Receiver, +) -> Result<(), String> { + send_message( + &mut stdin, + &json!({ + "jsonrpc": "2.0", + "id": 3, + "method": "shutdown", + "params": null + }), + )?; + + let _ = recv_until(messages_rx, |message| { + message.get("id").and_then(Value::as_i64) == Some(3) + }); + + send_message( + &mut stdin, + &json!({ + "jsonrpc": "2.0", + "method": "exit", + "params": null + }), + )?; + drop(stdin); + + for _ in 0..20 { + match child.try_wait() { + Ok(Some(status)) if status.success() => return Ok(()), + Ok(Some(status)) => { + return Err(format!("solcore-lsp exited with status {status}")); + } + Ok(None) => thread::sleep(Duration::from_millis(100)), + Err(error) => return Err(format!("failed waiting for solcore-lsp: {error}")), + } + } + + let _ = child.kill(); + let _ = child.wait(); + Err("solcore-lsp did not exit after shutdown/exit".to_owned()) +} + +fn recv_until( + messages_rx: &mpsc::Receiver, + mut predicate: impl FnMut(&Value) -> bool, +) -> Result { + for _ in 0..50 { + let message = messages_rx + .recv_timeout(Duration::from_secs(20)) + .map_err(|error| format!("timed out waiting for LSP message: {error}"))?; + if predicate(&message) { + return Ok(message); + } + } + + Err("did not receive expected LSP message within 50 messages".to_owned()) +} + +fn send_message(stdin: &mut ChildStdin, message: &Value) -> Result<(), String> { + let body = message.to_string(); + write!(stdin, "Content-Length: {}\r\n\r\n{body}", body.len()) + .map_err(|error| format!("failed to write LSP message header/body: {error}"))?; + stdin + .flush() + .map_err(|error| format!("failed to flush LSP message: {error}")) +} + +fn read_message(reader: &mut impl BufRead) -> io::Result { + let mut content_length = None; + loop { + let mut line = String::new(); + let bytes = reader.read_line(&mut line)?; + if bytes == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "stdout closed while reading LSP header", + )); + } + + let line = line.trim_end_matches(['\r', '\n']); + if line.is_empty() { + break; + } + + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = Some(value.trim().parse::().map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid content length: {error}"), + ) + })?); + } + } + + let content_length = content_length.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "missing Content-Length in LSP header", + ) + })?; + + let mut body = vec![0; content_length]; + reader.read_exact(&mut body)?; + serde_json::from_slice(&body).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) +} diff --git a/crates/nameres/Cargo.toml b/crates/nameres/Cargo.toml new file mode 100644 index 00000000..eb18d54d --- /dev/null +++ b/crates/nameres/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "solcore-nameres" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +salsa = { workspace = true } +rustc-hash = { workspace = true } +url = { workspace = true } +hir = { workspace = true } +parser = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +annotate-snippets = { workspace = true } diff --git a/crates/nameres/src/auto_import.rs b/crates/nameres/src/auto_import.rs new file mode 100644 index 00000000..52cdd839 --- /dev/null +++ b/crates/nameres/src/auto_import.rs @@ -0,0 +1,547 @@ +use super::*; + +/// Returns exact public-symbol matches that can be imported into `importing`. +/// +/// Providers whose export syntax cannot expose `public_name` are rejected +/// before computing their public interface. This exact-name path matters for +/// editor requests: expanding every embedded standard-library interface can +/// require substantially more stack than a JSON-RPC/WASM caller has available. +/// Wildcard exports remain conservative and are always expanded. +pub fn auto_import_candidates<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + public_name: &str, + namespace: Namespace, +) -> Vec> { + collect_auto_import_candidates(db, importing, Some((public_name, namespace))) +} + +/// Returns type imports that also expose `constructor_name` through +/// `type_name`. +/// +/// The initial type lookup deliberately reuses [`auto_import_candidates`], so +/// constructor fixes inherit its exact-name prefilter, selector-collision +/// checks, parse safety, canonical paths, and deterministic ranking. The +/// second pass only retains the corresponding public item reference when that +/// exact definition identity carries the requested visible constructor. +pub fn auto_import_constructor_candidates<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + type_name: &str, + constructor_name: &str, +) -> Vec> { + if !parser::is_valid_identifier(type_name) || !parser::is_valid_identifier(constructor_name) { + return Vec::new(); + } + + auto_import_candidates(db, importing, type_name, Namespace::Type) + .into_iter() + .filter(|candidate| { + public_interface(db, candidate.provider) + .item_refs + .iter() + .any(|item_ref| { + item_ref.namespace == Namespace::Type + && item_ref.public_name == candidate.public_name + && item_ref.origin == candidate.origin + && matches!( + &item_ref.constructors, + ConstructorVisibility::Visible(constructors) + if constructors.contains(constructor_name) + ) + }) + }) + .collect() +} + +/// Returns modules whose default import qualifier is `qualifier` and whose +/// public interface exposes the term `member` immediately below it. +/// +/// This intentionally handles only one ordinary identifier as the qualifier. +/// Alias synthesis and nested missing-prefix repair require choosing new local +/// spellings and are left to a higher-level refactoring. Every module binding +/// introduced by the canonical namespace form is checked. For example, +/// `import * as math from lib.one.math;` exposes `math`. A candidate is +/// suppressed if that name would conflict with a local/imported item or +/// another namespace import. +pub fn auto_import_module_candidates<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + qualifier: &str, + member: &str, +) -> Vec> { + if !parser::is_valid_identifier(qualifier) || !parser::is_valid_identifier(member) { + return Vec::new(); + } + + let importing_surface = module_import_surface(db, importing); + let syntactic_module_bindings = existing_plain_module_bindings(db, importing); + let snapshot = db.module_file_snapshot(); + let mut candidates = Vec::<(bool, AutoImportModuleCandidate<'db>)>::new(); + for key in snapshot.files(db).keys() { + let provider = module_id_from_key(db, key); + if provider == importing || module_has_parse_errors(db, provider) { + continue; + } + let Some(import_path) = source_import_path(db, importing, provider) else { + continue; + }; + let Some(generated_bindings) = generated_module_bindings(&import_path) else { + continue; + }; + if default_import_qualifier(&import_path) != Some(qualifier) + || module_bindings_conflict( + &importing_surface, + &syntactic_module_bindings, + &generated_bindings, + ) + || !module_may_export_name(db, provider, member) + { + continue; + } + + let interface = public_interface(db, provider); + let item_refs = interface + .item_refs + .iter() + .filter(|item_ref| { + item_ref.public_name == member && item_ref.namespace == Namespace::Term + }) + .collect::>(); + if item_refs.is_empty() { + continue; + } + if item_refs + .iter() + .any(|item_ref| module_has_parse_errors(db, item_ref.origin.module)) + || has_ambiguous_member_surface(&item_refs) + { + continue; + } + + let is_reexport = !item_refs + .iter() + .any(|item_ref| item_ref.origin.module == provider); + candidates.push(( + is_reexport, + AutoImportModuleCandidate { + provider, + import_path, + qualifier: qualifier.to_owned(), + member: member.to_owned(), + }, + )); + } + + candidates.sort_by(|(left_reexport, left), (right_reexport, right)| { + ( + left_reexport, + &left.import_path, + &left.qualifier, + &left.member, + ) + .cmp(&( + right_reexport, + &right.import_path, + &right.qualifier, + &right.member, + )) + }); + candidates.dedup_by(|(_, left), (_, right)| left == right); + candidates + .into_iter() + .map(|(_, candidate)| candidate) + .collect() +} + +fn default_import_qualifier(import_path: &str) -> Option<&str> { + let leaf = import_path.rsplit('.').next()?; + Some(leaf.strip_prefix('@').unwrap_or(leaf)) +} + +/// Computes the names introduced by the plain import that the LSP will emit. +/// +/// This mirrors `import_module_qualifiers` followed by `module_prefixes` for +/// canonical paths without allocating a synthetic HIR import. `lib` and a +/// multi-segment external-library root are source routing markers, not visible +/// qualifier segments. +fn generated_module_bindings(import_path: &str) -> Option> { + let external = import_path.starts_with('@'); + let path = import_path.strip_prefix('@').unwrap_or(import_path); + let segments = path.split('.').collect::>(); + if segments.is_empty() + || segments + .iter() + .any(|segment| !parser::is_valid_identifier(segment)) + { + return None; + } + let visible = if (external || segments.first() == Some(&"lib")) && segments.len() > 1 { + &segments[1..] + } else { + &segments[..] + }; + let leaf = visible.last()?.to_string(); + let full = visible.join("."); + Some(unique_strings( + unique_strings([leaf, full]) + .into_iter() + .flat_map(|qualifier| module_prefixes(&qualifier)), + )) +} + +/// Collects all bindings claimed by existing plain-import syntax, whether or +/// not the target currently resolves. Selective imports intentionally do not +/// participate: adding a separate plain import of the same provider is valid. +fn existing_plain_module_bindings<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, +) -> FxHashSet { + let Some(file) = db.module_file(importing) else { + return FxHashSet::default(); + }; + let mut bindings = FxHashSet::default(); + for import in module_imports(db, file).imports { + if import.selector(db).is_some() { + continue; + } + let path = path_ref_from_import(db, import); + for qualifier in import_module_qualifiers(db, import, &path) { + bindings.extend(module_prefixes(&qualifier)); + } + } + bindings +} + +fn module_bindings_conflict( + surface: &ModuleImportSurface<'_>, + syntactic_module_bindings: &FxHashSet, + generated_bindings: &[String], +) -> bool { + surface.unknown_unqualified_wildcard + || generated_bindings.iter().any(|name| { + syntactic_module_bindings.contains(name) + || surface.module_qualifiers.contains(name) + || surface.incomplete_modules.contains(name) + || surface.terms.contains_key(name) + || surface.types.contains_key(name) + || surface.unknown_unqualified_names.contains(name) + || surface.item_scope.as_ref().is_some_and(|scope| { + scope.terms.get(name).is_some() + || scope.types.get(name).is_some() + || scope.contracts.iter().any(|contract| { + contract.terms.get(name).is_some() + || contract.types.get(name).is_some() + || contract + .fields + .iter() + .any(|field| field.name == name.as_str()) + }) + }) + }) +} + +fn has_ambiguous_member_surface(item_refs: &[&ItemRef<'_>]) -> bool { + let mut origins = FxHashMap::, DefId<'_>)>>::default(); + for item_ref in item_refs { + origins + .entry(item_ref.namespace) + .or_default() + .insert((item_ref.origin.module, item_ref.origin.def_id)); + } + origins.values().any(|origins| origins.len() > 1) +} + +/// Builds the importable public-symbol index visible from `importing`. +/// +/// Parse-broken providers or origins and public names with an ambiguous +/// namespace-blind selector surface are omitted. Both cases can expose only a +/// provisional or ambiguous import and therefore cannot produce a safe +/// automatic edit. +#[salsa::tracked] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, importing), + fields(module = field::Empty, file = field::Empty) +)] +pub fn auto_import_index<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, +) -> Vec> { + record_module_field(db, importing); + collect_auto_import_candidates(db, importing, None) +} + +fn collect_auto_import_candidates<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + requested: Option<(&str, Namespace)>, +) -> Vec> { + let snapshot = db.module_file_snapshot(); + let selected_targets = selected_import_targets_by_local_name(db, importing); + let mut candidates = Vec::new(); + + for key in snapshot.files(db).keys() { + let provider = module_id_from_key(db, key); + if provider == importing || module_has_parse_errors(db, provider) { + continue; + } + let Some(import_path) = source_import_path(db, importing, provider) else { + continue; + }; + if requested + .is_some_and(|(public_name, _)| !module_may_export_name(db, provider, public_name)) + { + continue; + } + let interface = public_interface(db, provider); + let mut groups = BTreeMap::>>::new(); + for item_ref in &interface.item_refs { + if requested.is_some_and(|(public_name, _)| item_ref.public_name != public_name) { + continue; + } + groups + .entry(item_ref.public_name.clone()) + .or_default() + .push(item_ref); + } + + for (public_name, item_refs) in groups { + // Adding an unaliased selector exposes `public_name` locally. The + // import validator treats that local spelling as ambiguous when + // selected from different target modules, even if the two refs + // live in different namespaces. Do not offer an edit that would + // immediately introduce SC0120. A second selector from the same + // target remains allowed, matching validation's target identity. + if selected_targets + .get(&public_name) + .is_some_and(|targets| targets.iter().any(|target| *target != provider)) + { + continue; + } + + // Selective import syntax is namespace-blind. Require one + // definition identity across every namespace so choosing a term + // cannot silently bring in an unrelated same-named type (or vice + // versa). + let mut surface_origins = Vec::<(ModuleId<'db>, DefId<'db>, &str)>::new(); + for item_ref in &item_refs { + let key = ( + item_ref.origin.module, + item_ref.origin.def_id, + item_ref.source_name.as_str(), + ); + if !surface_origins.contains(&key) { + surface_origins.push(key); + } + } + if surface_origins.len() != 1 + || item_refs + .iter() + .any(|item_ref| module_has_parse_errors(db, item_ref.origin.module)) + { + continue; + } + + let mut by_namespace = BTreeMap::>>::new(); + for item_ref in item_refs { + by_namespace + .entry(namespace_sort_key(item_ref.namespace)) + .or_default() + .push(item_ref); + } + for item_refs in by_namespace.into_values() { + // `public_interface` merges identical refs. Multiple remaining + // refs in one namespace cannot identify one safe definition. + let [item_ref] = item_refs.as_slice() else { + continue; + }; + if requested.is_some_and(|(_, namespace)| item_ref.namespace != namespace) { + continue; + } + candidates.push(AutoImportCandidate { + provider, + import_path: import_path.clone(), + public_name: public_name.clone(), + namespace: item_ref.namespace, + origin: item_ref.origin.clone(), + }); + } + } + } + + candidates.sort_by(|left, right| { + ( + left.is_reexport(), + &left.import_path, + namespace_sort_key(left.namespace), + &left.public_name, + ) + .cmp(&( + right.is_reexport(), + &right.import_path, + namespace_sort_key(right.namespace), + &right.public_name, + )) + }); + candidates.dedup(); + candidates +} + +/// Resolves the local bindings introduced by the importing module's existing +/// selectors to the modules named by those import declarations. +/// +/// `select_import_refs` applies aliases, wildcard selection, and hiding, so +/// the resulting key is the actual local spelling that a generated unaliased +/// selector could collide with. Keeping targets rather than item origins is +/// intentional: this mirrors `validate_ambiguous_selected_imports`, including +/// its namespace-blind cross-target check for re-export providers. +fn selected_import_targets_by_local_name<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, +) -> FxHashMap>> { + let Some(file) = db.module_file(importing) else { + return FxHashMap::default(); + }; + let mut selected_targets = FxHashMap::>>::default(); + let mut scratch = Vec::new(); + for import in module_imports(db, file).imports { + let Some(selector) = import.selector(db) else { + continue; + }; + let path = path_ref_from_import(db, import); + let Some(target) = resolve_for_export( + db, + importing, + &path, + ExportResolutionMode::Lenient, + &mut scratch, + ) else { + continue; + }; + let interface = public_interface(db, target); + for item_ref in select_import_refs(db, &interface.item_refs, selector, import.hiding(db)) { + selected_targets + .entry(item_ref.public_name) + .or_default() + .insert(target); + } + } + selected_targets +} + +/// Conservatively decides whether a module's export declarations can expose +/// `public_name` without expanding any imported/re-exported interfaces. +fn module_may_export_name<'db>(db: &'db dyn Db, module: ModuleId<'db>, public_name: &str) -> bool { + let Some(file) = db.module_file(module) else { + return false; + }; + module_imports(db, file).exports.iter().any(|export| { + let names = match export.kind(db) { + ExportKind::List(names) | ExportKind::ItemsFrom(_, names) => names, + // These forms expose only a module qualifier, never an item ref. + ExportKind::Module(_) | ExportKind::ModuleAs(_, _) => return false, + }; + names.iter().any(|exported| { + let name = spanned_name_text(db, &exported.name); + name == public_name || name == "*" || name.ends_with(".*") + }) + }) +} + +/// Produces the canonical source-level module path for importing `provider` +/// from `importing`. +/// +/// Main-library paths are absolute `lib.*` paths and are available only within +/// the same workspace/detached-root namespace. Internal namespace segments are +/// stripped from the returned text. Standard and configured external libraries +/// use their global `std.*` and `@name.*` spellings. `None` is returned when a +/// logical path cannot be represented by ordinary source identifiers. +pub fn source_import_path<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + provider: ModuleId<'db>, +) -> Option { + if importing == provider { + return None; + } + match provider.library(db) { + LibraryId::Main => { + if importing.library(db) != &LibraryId::Main { + return None; + } + let importing_prefix = main_workspace_prefix(importing.logical_path(db)); + let provider_prefix = main_workspace_prefix(provider.logical_path(db)); + if importing_prefix != provider_prefix { + return None; + } + let relative = &provider.logical_path(db)[provider_prefix.len()..]; + valid_module_segments(relative).then(|| format!("lib.{}", relative.join("."))) + } + LibraryId::Std => { + let path = provider.logical_path(db); + if path.as_slice() == ["std"] { + Some("std".to_owned()) + } else if !valid_module_segments(path) { + None + } else { + Some(format!("std.{}", path.join("."))) + } + } + LibraryId::External(name) => { + if !parser::is_valid_identifier(name) + || !db.module_tree().external_roots(db).contains_key(name) + { + return None; + } + let path = provider.logical_path(db); + if path.first() == Some(name) && path.len() == 1 { + Some(format!("@{name}")) + } else if !valid_module_segments(path) { + None + } else { + Some(format!("@{name}.{}", path.join("."))) + } + } + } +} + +fn valid_module_segments(segments: &[String]) -> bool { + !segments.is_empty() + && segments + .iter() + .all(|segment| parser::is_valid_identifier(segment)) +} + +#[cfg(test)] +mod tests { + use super::generated_module_bindings; + + #[test] + fn canonical_paths_expose_the_same_module_prefixes_as_plain_imports() { + assert_eq!( + generated_module_bindings("lib.one.math").as_deref(), + Some(&["math".to_owned(), "one".to_owned(), "one.math".to_owned()][..]) + ); + assert_eq!( + generated_module_bindings("std.collections.list").as_deref(), + Some( + &[ + "list".to_owned(), + "std".to_owned(), + "std.collections".to_owned(), + "std.collections.list".to_owned(), + ][..] + ) + ); + assert_eq!( + generated_module_bindings("@pkg.math.api").as_deref(), + Some(&["api".to_owned(), "math".to_owned(), "math.api".to_owned()][..]) + ); + assert_eq!( + generated_module_bindings("@pkg").as_deref(), + Some(&["pkg".to_owned()][..]) + ); + } +} diff --git a/crates/nameres/src/diagnostics.rs b/crates/nameres/src/diagnostics.rs new file mode 100644 index 00000000..3032bccc --- /dev/null +++ b/crates/nameres/src/diagnostics.rs @@ -0,0 +1,771 @@ +use super::*; + +/// Typed inter-module diagnostic. +/// +/// These variants cover module loading, import validation, export validation, +/// and import-surface conflicts. They stay typed while the `solcore-nameres` +/// crate computes module state, then lower to the generic diagnostic surface +/// for aggregation and rendering. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum ModuleDiagnostic<'db> { + /// `SC0109`: a module path resolved to no loaded source file. + ModuleNotFound { + /// Display form of the missing module path. + path: String, + /// Span of the module reference. + span: LabelSpan, + /// Nearest existing module path, when one is close enough. + suggestion: Option, + }, + /// `SC0110`: selected or hidden import item is absent from the target. + UnknownImportItem { + /// Missing imported item name. + name: String, + /// Span of the selected or hidden name. + span: LabelSpan, + /// Target module that does not export the item. + module: Option, + /// Nearest exported item, when one is close enough. + suggestion: Option, + }, + /// `SC0111`: two exported items expose the same public name. + DuplicateExportedItemName { + /// Duplicated exported item name. + name: String, + /// Optional export declaration/name span. + span: Option, + }, + /// `SC0112`: two exported module aliases expose the same public name. + DuplicateExportedModuleName { + /// Duplicated exported module alias. + name: String, + /// Optional export declaration/name span. + span: Option, + }, + /// `SC0113`: a local export names no local or selected import item. + UnknownLocalExport { + /// Missing export name. + name: String, + /// Span of the export name. + span: LabelSpan, + }, + /// `SC0114`: an exported constructor is absent from the exported type. + UnknownLocalConstructor { + /// Exported type name. + type_name: String, + /// Missing constructor name. + ctor_name: String, + /// Span of the exported type name. + span: LabelSpan, + }, + /// `SC0115`: a re-export names no item provided by the target module. + UnknownReExport { + /// Missing re-exported name. + name: String, + /// Span of the re-exported name. + span: LabelSpan, + }, + /// `SC0115`: a re-exported constructor is absent from the target type. + UnknownReExportConstructor { + /// Re-exported type name. + type_name: String, + /// Missing constructor name. + ctor_name: String, + /// Span of the re-exported type name. + span: LabelSpan, + }, + /// `SC0116`: two plain imports introduce the same qualifier. + DuplicateImportQualifier { + /// Duplicated qualifier name. + name: String, + /// Span of the first qualifier. + first: LabelSpan, + /// Span of the duplicate qualifier. + second: LabelSpan, + }, + /// `SC0117`: a selective import lists the same effective name twice. + DuplicateImportSelector { + /// Duplicated selected or hidden name. + name: String, + /// Span of the first occurrence. + first: LabelSpan, + /// Span of the duplicate occurrence. + second: LabelSpan, + }, + /// `SC0118`: an external-library path has no configured root. + MissingExternalRoot { + /// External library name. + name: String, + /// Span of the external import marker or path. + span: LabelSpan, + }, + /// `SC0120`: the same selected name is imported from multiple modules. + AmbiguousSelectedImport { + /// Namespace context that made the selected public name ambiguous. + namespaces: Vec, + /// Ambiguous selected name. + name: String, + /// Optional span of the import that introduced the ambiguity. + span: Option, + /// Modules that provide the same name. + modules: Vec>, + }, + /// `SC0121`: an unqualified import surface conflicts with a local name. + ConflictingUnqualifiedName { + /// Conflicting name. + name: String, + /// Span of the import that introduced the name. + import_span: LabelSpan, + /// Span of the local binding with the same name. + local_span: LabelSpan, + }, +} + +impl<'db> ModuleDiagnostic<'db> { + /// Lowers this typed module diagnostic to the generic rendering surface. + pub fn lower(&self, db: &'db dyn Db) -> Diagnostic { + match self { + ModuleDiagnostic::ModuleNotFound { + path, + span, + suggestion, + } => { + let mut diagnostic = Diagnostic::error(format!("import {path}: file not found")) + .with_code(DiagnosticCode::MODULE_NOT_FOUND) + .with_primary_label_span(span.clone(), Some("module reference")) + .with_help("check the module path or add the missing source file"); + if let Some(suggestion) = suggestion { + diagnostic = diagnostic + .with_help(format!("did you mean `{suggestion}`?")) + .with_suggestion(replace_with_suggestion(span, suggestion)); + } + diagnostic + } + ModuleDiagnostic::UnknownImportItem { + name, + span, + module, + suggestion, + } => { + let mut diagnostic = Diagnostic::error(format!("unknown import item `{name}`")) + .with_code(DiagnosticCode::MODULE_UNKNOWN_IMPORT_ITEM) + .with_primary_label_span(span.clone(), Some("unknown import item")); + if let Some(module) = module { + diagnostic = diagnostic + .with_note(format!("`{name}` is not exported by module `{module}`")); + } + if let Some(suggestion) = suggestion { + diagnostic = diagnostic + .with_help(format!("did you mean `{suggestion}`?")) + .with_suggestion(replace_with_suggestion(span, suggestion)); + } + diagnostic.with_help("check the imported module's exported names") + } + ModuleDiagnostic::DuplicateExportedItemName { name, span } => { + let diagnostic = + Diagnostic::error(format!("duplicate exported item name `{name}`")) + .with_code(DiagnosticCode::MODULE_DUPLICATE_EXPORTED_ITEM_NAME) + .with_note("export each item name from only one origin"); + if let Some(span) = span { + diagnostic.with_primary_label_span( + span.clone(), + Some("module exports this name more than once"), + ) + } else { + diagnostic + } + } + ModuleDiagnostic::DuplicateExportedModuleName { name, span } => { + let diagnostic = + Diagnostic::error(format!("duplicate exported module name `{name}`")) + .with_code(DiagnosticCode::MODULE_DUPLICATE_EXPORTED_MODULE_NAME) + .with_note("export each module name from only one target"); + if let Some(span) = span { + diagnostic.with_primary_label_span( + span.clone(), + Some("module exports this alias more than once"), + ) + } else { + diagnostic + } + } + ModuleDiagnostic::UnknownLocalExport { name, span } => { + Diagnostic::error(format!("unknown export `{name}`")) + .with_code(DiagnosticCode::MODULE_UNKNOWN_LOCAL_EXPORT) + .with_primary_label_span(span.clone(), Some("unknown export")) + .with_note( + "export a top-level item defined in this module or selected from an import", + ) + } + ModuleDiagnostic::UnknownLocalConstructor { + type_name, + ctor_name, + span, + } => Diagnostic::error(format!( + "unknown exported constructor `{type_name}.{ctor_name}`" + )) + .with_code(DiagnosticCode::MODULE_UNKNOWN_LOCAL_CONSTRUCTOR) + .with_primary_label_span(span.clone(), Some("unknown exported constructor")) + .with_note("select constructors defined by the exported type"), + ModuleDiagnostic::UnknownReExport { name, span } => { + Diagnostic::error(format!("unknown re-exported name `{name}`")) + .with_code(DiagnosticCode::MODULE_UNKNOWN_REEXPORT) + .with_primary_label_span(span.clone(), Some("unknown re-exported name")) + .with_note("re-export a name provided by the target module") + } + ModuleDiagnostic::UnknownReExportConstructor { + type_name, + ctor_name, + span, + } => Diagnostic::error(format!( + "unknown re-exported constructor `{type_name}.{ctor_name}`" + )) + .with_code(DiagnosticCode::MODULE_UNKNOWN_REEXPORT_CONSTRUCTOR) + .with_primary_label_span(span.clone(), Some("unknown re-exported constructor")) + .with_note("re-export constructors provided by the target module"), + ModuleDiagnostic::DuplicateImportQualifier { + name, + first, + second, + } => Diagnostic::error(format!("duplicate import qualifier `{name}`")) + .with_code(DiagnosticCode::MODULE_DUPLICATE_IMPORT_QUALIFIER) + .with_primary_label_span(second.clone(), Some("duplicate import qualifier")) + .with_secondary_label_span(first.clone(), Some("first qualifier with this name")) + .with_note("use an explicit alias to disambiguate one of the imports"), + ModuleDiagnostic::DuplicateImportSelector { + name, + first, + second, + } => Diagnostic::error(format!("duplicate name `{name}` in selective import")) + .with_code(DiagnosticCode::MODULE_DUPLICATE_IMPORT_SELECTOR) + .with_primary_label_span(second.clone(), Some("duplicate selected import")) + .with_secondary_label_span( + first.clone(), + Some("first selected import with this name"), + ) + .with_note("list each selected or hidden name only once"), + ModuleDiagnostic::MissingExternalRoot { name, span } => { + Diagnostic::error(format!("external library root is not configured: @{name}")) + .with_code(DiagnosticCode::MODULE_MISSING_EXTERNAL_ROOT) + .with_primary_label_span(span.clone(), Some("external library import")) + .with_note("configure the external library root") + } + ModuleDiagnostic::AmbiguousSelectedImport { + namespaces, + name, + span, + modules, + } => { + let module_list = module_list_display(db, modules); + let context = namespace_context(namespaces); + let label = format!("ambiguous selected import {context}"); + let mut diagnostic = + Diagnostic::error(format!("ambiguous selected import `{name}` {context}")) + .with_code(DiagnosticCode::MODULE_AMBIGUOUS_SELECTED_IMPORT); + if let Some(span) = span { + diagnostic = diagnostic.with_primary_label_span(span.clone(), Some(label)); + } + diagnostic + .with_note(format!("`{name}` is imported from {module_list} {context}")) + .with_note("use an explicit module qualifier or narrow the selected imports") + } + ModuleDiagnostic::ConflictingUnqualifiedName { + name, + import_span, + local_span, + } => Diagnostic::error(format!("conflicting unqualified name `{name}`")) + .with_code(DiagnosticCode::MODULE_CONFLICTING_UNQUALIFIED_NAME) + .with_primary_label_span(import_span.clone(), Some("conflicting imported name")) + .with_secondary_label_span(local_span.clone(), Some("local binding with this name")) + .with_note("rename the local binding or use an import alias"), + } + } +} + +fn replace_with_suggestion(span: &LabelSpan, replacement: &str) -> Suggestion { + Suggestion { + title: format!("Replace with `{replacement}`"), + applicability: Applicability::MaybeIncorrect, + edits: vec![AnchoredTextEdit { + span: span.clone(), + replacement: replacement.to_owned(), + }], + } +} + +fn module_list_display<'db>(db: &'db dyn Db, modules: &[ModuleId<'db>]) -> String { + use std::fmt::Write as _; + + let mut result = String::new(); + for module in modules { + if !result.is_empty() { + result.push_str(", "); + } + let _ = write!(&mut result, "{}", module.display(db)); + } + result +} + +#[salsa::tracked(returns(ref))] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, module), + fields(module = field::Empty, file = field::Empty) +)] +pub fn module_diagnostics<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> Vec { + record_module_field(db, module); + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + + let mut diagnostics = parse_diagnostics(db, file).to_vec(); + let has_parse_errors = !diagnostics.is_empty(); + if has_parse_errors { + // A parse-broken file has incomplete recovered HIR. The reference + // compiler stops before nameres in this state, so we publish only parse + // diagnostics here while still allowing resolution queries to run for + // editor features. + sort_dedup_query_diagnostics(db, &mut diagnostics); + return diagnostics; + } + + let mut module_diags = collect_module_validation_diagnostics(db, module); + let env = module_env(db, module); + module_diags.extend(env.diagnostics.iter().cloned()); + diagnostics.extend( + module_diags + .into_iter() + .map(|diagnostic| AnyDiagnostic::Module(diagnostic.lower(db))), + ); + + if !matches!(module.library(db), LibraryId::Std) { + let hir_module = parse_file_to_hir(db, file).module(db); + if let Some(item_scope) = env.item_scope.clone() { + diagnostics.extend( + item_scope + .diagnostics + .iter() + .cloned() + .map(AnyDiagnostic::Nameres), + ); + let item_resolutions = + hir_nameres::resolve_item_types_with_imports(db, hir_module, &item_scope, &env); + diagnostics.extend( + item_resolutions + .diagnostics + .iter() + .cloned() + .map(AnyDiagnostic::Nameres), + ); + collect_body_diagnostics( + db, + hir_module, + &env, + BodyDiagnosticPolicy::from_parse_errors(has_parse_errors), + &mut diagnostics, + ); + } + } + + sort_dedup_query_diagnostics(db, &mut diagnostics); + diagnostics +} + +/// Returns local name-resolution diagnostics for one function body. +#[salsa::tracked(returns(ref))] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, body, context, surface), + fields(file = field::Empty, def = field::Empty) +)] +pub fn body_diagnostics<'db>( + db: &'db dyn Db, + body: FuncBody<'db>, + context: hir_nameres::BodyResolutionContext<'db>, + surface: ModuleImportSurface<'db>, + suppress_for_parse_errors: bool, +) -> Vec { + record_body_field(db, body); + let policy = BodyDiagnosticPolicy::from_suppress_for_parse_errors(suppress_for_parse_errors); + let resolution = hir_nameres::resolve_body_with_imports_and_policy( + db, + body, + &context, + &surface, + policy.as_hir_policy(), + ); + let mut diagnostics = resolution + .diagnostics + .into_iter() + .filter(|diagnostic| !is_suppressed_unknown_diagnostic(&surface, diagnostic)) + .map(AnyDiagnostic::Nameres) + .collect::>(); + sort_dedup_query_diagnostics(db, &mut diagnostics); + diagnostics +} + +fn is_suppressed_unknown_diagnostic( + surface: &ModuleImportSurface<'_>, + diagnostic: &hir_nameres::NameresDiagnostic, +) -> bool { + match diagnostic { + hir_nameres::NameresDiagnostic::UndefinedName { name, .. } => { + surface.unknown_unqualified_wildcard || surface.unknown_unqualified_names.contains(name) + } + _ => false, + } +} + +fn collect_body_diagnostics<'db>( + db: &'db dyn Db, + module: Module<'db>, + env: &ModuleEnv<'db>, + policy: BodyDiagnosticPolicy, + diagnostics: &mut Vec, +) { + let mut collector = BodyDiagnosticCollector { + db, + module, + env, + policy, + diagnostics, + }; + for item in module.items(db) { + collector.item(*item, None, &[]); + } +} + +struct BodyDiagnosticCollector<'a, 'db> { + db: &'db dyn Db, + module: Module<'db>, + env: &'a ModuleEnv<'db>, + policy: BodyDiagnosticPolicy, + diagnostics: &'a mut Vec, +} + +impl<'a, 'db> BodyDiagnosticCollector<'a, 'db> { + fn item( + &mut self, + item: Item<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + match item { + Item::FunctionDef(def) => { + self.function(def, enclosing_contract, inherited_type_vars); + } + Item::InstanceDef(def) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + def.def_id_value(self.db), + def.type_var_elems(self.db), + )); + for method in def.methods(self.db) { + self.function(*method, enclosing_contract, &inherited); + } + } + Item::ContractDef(def) => { + let mut inherited = inherited_type_vars.to_vec(); + inherited.extend(type_var_bindings( + def.def_id_value(self.db), + def.ty_param_elems(self.db), + )); + for item in def.items(self.db) { + match *item { + ContractItem::FunctionDef(defn) => { + self.function(defn, Some(def.def_id_value(self.db)), &inherited); + } + ContractItem::TypeAlias(_) + | ContractItem::AdtDef(_) + | ContractItem::Error { .. } => {} + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + fn function( + &mut self, + function: FunctionDef<'db>, + enclosing_contract: Option>, + inherited_type_vars: &[hir_nameres::TypeVarBinding<'db>], + ) { + let Some(body) = function.body(self.db) else { + return; + }; + let sig = function.sig(self.db); + let mut type_vars = inherited_type_vars.to_vec(); + type_vars.extend(type_var_bindings( + function.def_id_value(self.db), + &sig.type_vars, + )); + let context = hir_nameres::BodyResolutionContext { + module: self.module, + enclosing_contract, + params: param_bindings(sig.params.atom()), + type_vars, + }; + self.diagnostics.extend( + body_diagnostics( + self.db, + body, + context, + self.env.import_surface(), + self.policy.suppress_for_parse_errors(), + ) + .iter() + .cloned(), + ); + } +} + +/// Returns diagnostics for every module reachable from `entry`. +#[salsa::tracked(returns(ref))] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, entry), + fields(module = field::Empty, file = field::Empty) +)] +pub fn reachable_diagnostics<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> Vec { + record_module_field(db, entry); + let mut diagnostics = Vec::new(); + for module in reachable_modules(db, entry) { + diagnostics.extend(module_diagnostics(db, module).iter().cloned()); + } + sort_dedup_query_diagnostics(db, &mut diagnostics); + diagnostics +} + +fn collect_module_validation_diagnostics<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Vec> { + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + let module_items = module_imports(db, file); + let mut diagnostics = Vec::new(); + + for path in module_items + .import_refs + .iter() + .chain(module_items.export_refs.iter()) + { + if let Err(diagnostic) = resolve_module_path(db, module, path.clone()) { + diagnostics.push(*diagnostic); + } + } + + validate_imports(db, module, &mut diagnostics); + let _ = public_interface(db, module); + let raw = expand_module_exports(db, module, ExportResolutionMode::Strict, &mut diagnostics); + validate_duplicate_exports(db, module, &raw, &mut diagnostics); + diagnostics +} + +fn param_bindings<'db>(params: &[FuncParam<'db>]) -> Vec> { + params + .iter() + .filter_map(param_name) + .map(|name| hir_nameres::ParamBinding { name: *name }) + .collect() +} + +fn param_name<'a, 'db>(param: &'a FuncParam<'db>) -> Option<&'a SpannedElem<'db, Ident<'db>>> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => Some(name), + FuncParam::Error { .. } => None, + } +} + +fn type_var_bindings<'db>( + owner: DefId<'db>, + vars: &[SpannedElem<'db, Ident<'db>>], +) -> Vec> { + vars.iter() + .enumerate() + .map(|(index, name)| hir_nameres::TypeVarBinding { + owner, + name: *name, + index: index as u32, + }) + .collect() +} + +pub(super) fn module_root_span<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> Option> { + let file = db.module_file(module)?; + let anchor = AnchorId::root(db, file); + Some(Span::new(anchor, Offset::new(0), Offset::new(0))) +} + +pub(super) fn module_not_found_diag<'db>( + db: &'db dyn Db, + path: &ModulePathRef<'db>, + suggestion: Option, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::ModuleNotFound { + path: module_path_display(db, path), + span: LabelSpan::from_span(db, module_path_span(db, path)), + suggestion, + } +} + +pub(super) fn missing_external_root_diag<'db>( + db: &'db dyn Db, + path: &ModulePathRef<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::MissingExternalRoot { + name: name.to_owned(), + span: LabelSpan::from_span(db, path.external.unwrap_or(path.span)), + } +} + +pub(super) fn unknown_import_item_diag<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, + module: Option>, + suggestion: Option, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::UnknownImportItem { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + module: module.map(|module| module_id_display(db, module)), + suggestion, + } +} + +pub(super) fn duplicate_qualifier_diag<'db>( + db: &'db dyn Db, + first: Span<'db>, + second: Span<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::DuplicateImportQualifier { + name: name.to_owned(), + first: LabelSpan::from_span(db, first), + second: LabelSpan::from_span(db, second), + } +} + +pub(super) fn duplicate_selector_diag<'db>( + db: &'db dyn Db, + first: Span<'db>, + second: Span<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::DuplicateImportSelector { + name: name.to_owned(), + first: LabelSpan::from_span(db, first), + second: LabelSpan::from_span(db, second), + } +} + +pub(super) fn ambiguous_import_diag<'db>( + db: &'db dyn Db, + span: Option>, + namespaces: &[Namespace], + name: &str, + modules: Vec>, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::AmbiguousSelectedImport { + namespaces: namespaces.to_vec(), + name: name.to_owned(), + span: span.map(|span| LabelSpan::from_span(db, span)), + modules, + } +} + +pub(super) fn conflicting_unqualified_name_diag<'db>( + db: &'db dyn Db, + import_span: Span<'db>, + local_span: Span<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::ConflictingUnqualifiedName { + name: name.to_owned(), + import_span: LabelSpan::from_span(db, import_span), + local_span: LabelSpan::from_span(db, local_span), + } +} + +pub(super) fn unknown_local_export_diag<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::UnknownLocalExport { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn unknown_local_ctor_diag<'db>( + db: &'db dyn Db, + span: Span<'db>, + type_name: &str, + ctor_name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::UnknownLocalConstructor { + type_name: type_name.to_owned(), + ctor_name: ctor_name.to_owned(), + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn unknown_reexport_diag<'db>( + db: &'db dyn Db, + span: Span<'db>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::UnknownReExport { + name: name.to_owned(), + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn unknown_reexport_ctor_diag<'db>( + db: &'db dyn Db, + span: Span<'db>, + type_name: &str, + ctor_name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::UnknownReExportConstructor { + type_name: type_name.to_owned(), + ctor_name: ctor_name.to_owned(), + span: LabelSpan::from_span(db, span), + } +} + +pub(super) fn duplicate_export_item_diag<'db>( + db: &'db dyn Db, + span: Option>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::DuplicateExportedItemName { + name: name.to_owned(), + span: span.map(|span| LabelSpan::from_span(db, span)), + } +} + +pub(super) fn duplicate_export_module_diag<'db>( + db: &'db dyn Db, + span: Option>, + name: &str, +) -> ModuleDiagnostic<'db> { + ModuleDiagnostic::DuplicateExportedModuleName { + name: name.to_owned(), + span: span.map(|span| LabelSpan::from_span(db, span)), + } +} diff --git a/crates/nameres/src/env.rs b/crates/nameres/src/env.rs new file mode 100644 index 00000000..5d56e0cc --- /dev/null +++ b/crates/nameres/src/env.rs @@ -0,0 +1,634 @@ +use super::*; + +/// Builds the imported-name environment for a module. +/// +/// Missing source files produce an empty environment so graph/load errors can +/// be reported separately without panicking downstream HIR resolution. +#[salsa::tracked] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, module), + fields(module = field::Empty, file = field::Empty) +)] +pub fn module_env<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> ModuleEnv<'db> { + record_module_field(db, module); + let Some(file) = db.module_file(module) else { + return ModuleEnv::empty(); + }; + let hir_module = parse_file_to_hir(db, file).module(db); + let item_scope = hir_nameres::item_scope(db, hir_module); + let imports = module_imports(db, file); + let instances = instance_imports(db, module); + let mut builder = ModuleEnvBuilder::new(db, module, item_scope, instances); + for import in imports.imports { + builder.add_import(import); + } + builder.finish() +} + +/// Builds an imported-name environment for an already parsed HIR module. +/// +/// The logical `module` is still used to resolve relative import paths, while +/// imports and local item scope come directly from `hir_module`. This is the +/// semantic entry point for compiler-owned overlays whose imports do not exist +/// in the source file. +pub fn module_env_for_hir_module<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + hir_module: Module<'db>, +) -> ModuleEnv<'db> { + let item_scope = hir_nameres::item_scope(db, hir_module); + let imports = hir_module + .items(db) + .iter() + .filter_map(|item| match item { + Item::Import(import) => Some(*import), + _ => None, + }) + .collect::>(); + let instances = instance_imports_for_hir_module(db, module, hir_module); + let mut builder = ModuleEnvBuilder::new(db, module, item_scope, instances); + for import in imports { + builder.add_import(import); + } + builder.finish() +} + +/// Returns imported-name facts for a module without diagnostics. +#[salsa::tracked] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, module), + fields(module = field::Empty, file = field::Empty) +)] +pub fn module_import_surface<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> ModuleImportSurface<'db> { + record_module_field(db, module); + module_env(db, module).import_surface() +} + +pub(super) fn module_has_parse_errors<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> bool { + db.module_file(module) + .is_some_and(|file| !parse_diagnostics(db, file).is_empty()) +} + +/// Runs validation and HIR name resolution for one module. +/// +/// Standard library modules are currently validated but skipped for full local +/// HIR body resolution to keep driver runs focused on user code. +#[salsa::tracked] +pub fn resolve_module_full<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> FullResolutionSummary { + let _ = validate_module(db, module); + if matches!(module.library(db), LibraryId::Std) { + return FullResolutionSummary { checked: true }; + } + let Some(file) = db.module_file(module) else { + return FullResolutionSummary { checked: true }; + }; + let hir_module = parse_file_to_hir(db, file).module(db); + let env = module_env(db, module); + if let Some(item_scope) = env.item_scope.clone() { + let policy = if module_has_parse_errors(db, module) { + hir_nameres::NameresDiagnosticPolicy::SuppressForParseErrors + } else { + hir_nameres::NameresDiagnosticPolicy::Emit + }; + let _ = hir_nameres::resolve_module_with_imports_and_policy( + db, hir_module, item_scope, &env, policy, + ); + } + FullResolutionSummary { checked: true } +} + +struct ModuleEnvBuilder<'db> { + db: &'db dyn Db, + module: ModuleId<'db>, + env: ModuleEnv<'db>, + local_terms: FxHashMap>, + local_types: FxHashMap>, + imported_terms: FxHashMap>, + module_binding_spans: FxHashMap>, + conflict_diagnostics: FxHashSet<(hir_nameres::Namespace, String)>, + module_conflict_diagnostics: FxHashSet, +} + +impl<'db> ModuleEnvBuilder<'db> { + fn new( + db: &'db dyn Db, + module: ModuleId<'db>, + item_scope: hir_nameres::ItemScope<'db>, + instances: InstanceImports<'db>, + ) -> Self { + let owner = item_scope.module.def_id_value(db); + let item_scope_facts = item_scope.facts(); + let local_terms = item_scope + .terms + .iter() + .map(|entry| (entry.name.clone(), entry.span)) + .collect(); + let local_types = item_scope + .types + .iter() + .map(|entry| (entry.name.clone(), entry.span)) + .collect(); + Self { + db, + module, + env: ModuleEnv { + surface: ModuleImportSurface { + owner: Some(owner), + item_scope: Some(item_scope_facts), + terms: BTreeMap::new(), + types: BTreeMap::new(), + modules: BTreeMap::new(), + module_qualifiers: BTreeSet::new(), + module_origins: BTreeMap::new(), + constructor_leaves: BTreeSet::new(), + constructor_visibility: BTreeMap::new(), + partial_data: BTreeMap::new(), + unknown_unqualified_names: BTreeSet::new(), + unknown_unqualified_wildcard: false, + incomplete_modules: BTreeSet::new(), + private_surfaces: BTreeMap::new(), + instances: unique_origins( + instances.local.into_iter().chain(instances.imported), + ), + }, + item_scope: Some(item_scope), + diagnostics: Vec::new(), + }, + local_terms, + local_types, + imported_terms: FxHashMap::default(), + module_binding_spans: FxHashMap::default(), + conflict_diagnostics: FxHashSet::default(), + module_conflict_diagnostics: FxHashSet::default(), + } + } + + fn finish(mut self) -> ModuleEnv<'db> { + let module_bindings = self + .module_binding_spans + .iter() + .map(|(name, span)| (name.clone(), *span)) + .collect::>(); + for (name, span) in module_bindings { + self.check_module_name_conflict(&name, span); + } + self.env + } + + fn add_import(&mut self, import: Import<'db>) { + let path = path_ref_from_import(self.db, import); + let selector = import.selector(self.db); + let Ok(target) = resolve_module_path(self.db, self.module, path.clone()) else { + if let Some(selector) = selector.as_ref() { + self.add_unknown_selector_imports(selector); + } + return; + }; + let target_has_parse_errors = module_has_parse_errors(self.db, target); + tracing::trace!( + target: "nameres::imports", + module = %self.module.display(self.db), + path = %ModulePathDisplay::new(self.db, &path), + target = %target.display(self.db), + selector = selector.as_ref().map(selector_kind).unwrap_or("module"), + target_has_parse_errors, + "building import surface" + ); + + if let Some(selector) = selector.as_ref() { + if target_has_parse_errors { + self.add_unknown_selector_imports(selector); + } + let interface = public_interface(self.db, target); + self.add_unknown_missing_selector_imports(selector, &interface); + let item_refs = select_import_refs( + self.db, + &interface.item_refs, + selector, + import.hiding(self.db), + ); + tracing::trace!( + target: "nameres::imports", + module = %self.module.display(self.db), + target = %target.display(self.db), + selected = item_refs.len(), + "selected import refs" + ); + for item_ref in item_refs { + self.add_selected_item_ref(item_ref, import.span(self.db)); + } + return; + } + + let qualifiers = import_module_qualifiers(self.db, import, &path); + tracing::trace!( + target: "nameres::imports", + module = %self.module.display(self.db), + target = %target.display(self.db), + qualifiers = qualifiers.len(), + "resolved module import qualifiers" + ); + for qualifier in qualifiers { + let mut seen = FxHashSet::default(); + let mut stack = FxHashSet::default(); + self.add_module_surface( + &qualifier, + target, + import.span(self.db), + &mut seen, + &mut stack, + ); + } + } + + fn add_unknown_selector_imports(&mut self, selector: &ImportSelector<'db>) { + match selector { + ImportSelector::Wildcard => { + self.env.unknown_unqualified_wildcard = true; + } + ImportSelector::Names(names) => { + for selected in names { + let local_name = selected + .alias + .as_ref() + .map(|alias| spanned_name_text(self.db, alias)) + .unwrap_or_else(|| spanned_name_text(self.db, &selected.name)); + self.env.unknown_unqualified_names.insert(local_name); + } + } + } + } + + fn add_unknown_missing_selector_imports( + &mut self, + selector: &ImportSelector<'db>, + interface: &Interface<'db>, + ) { + let ImportSelector::Names(names) = selector else { + return; + }; + let available = interface_names(interface); + for selected in names { + let source_name = spanned_name_text(self.db, &selected.name); + if available.contains(&source_name) { + continue; + } + let local_name = selected + .alias + .as_ref() + .map(|alias| spanned_name_text(self.db, alias)) + .unwrap_or(source_name); + self.env.unknown_unqualified_names.insert(local_name); + } + } + + fn add_selected_item_ref(&mut self, item_ref: ItemRef<'db>, span: Span<'db>) { + self.check_selected_conflict(&item_ref, span); + if item_ref.namespace == Namespace::Term && !item_ref.public_name.contains('.') { + self.imported_terms + .entry(item_ref.public_name.clone()) + .or_insert(span); + } + self.add_item_ref_surface(&item_ref, None); + } + + fn check_selected_conflict(&mut self, item_ref: &ItemRef<'db>, span: Span<'db>) { + let namespace = match item_ref.namespace { + Namespace::Term => hir_nameres::Namespace::Term, + Namespace::Type | Namespace::Class => hir_nameres::Namespace::Type, + }; + let local_span = match namespace { + hir_nameres::Namespace::Term => self.local_terms.get(&item_ref.public_name), + hir_nameres::Namespace::Type => self.local_types.get(&item_ref.public_name), + hir_nameres::Namespace::Field | hir_nameres::Namespace::Module => None, + }; + if let Some(local_span) = local_span + && self + .conflict_diagnostics + .insert((namespace, item_ref.public_name.clone())) + { + self.push_duplicate_import_diagnostic( + namespace, + &item_ref.public_name, + *local_span, + span, + ); + } + } + + fn push_duplicate_import_diagnostic( + &mut self, + namespace: hir_nameres::Namespace, + name: &str, + local_span: Span<'db>, + import_span: Span<'db>, + ) { + if let Some(item_scope) = &mut self.env.item_scope { + item_scope + .diagnostics + .push(hir_nameres::NameresDiagnostic::DuplicateDeclaration { + namespace, + name: name.to_owned(), + span: LabelSpan::from_span(self.db, local_span), + previous: LabelSpan::from_span(self.db, import_span), + context: None, + }); + } + } + + fn add_module_surface( + &mut self, + qualifier: &str, + target: ModuleId<'db>, + span: Span<'db>, + seen: &mut FxHashSet<(String, ModuleId<'db>)>, + stack: &mut FxHashSet>, + ) { + self.add_module_binding(qualifier, target, span); + + if !seen.insert((qualifier.to_owned(), target)) { + tracing::trace!( + target: "nameres::imports", + module = %self.module.display(self.db), + qualifier, + target = %target.display(self.db), + "skipped repeated module surface" + ); + return; + } + + let interface = public_interface(self.db, target); + for item_ref in &interface.item_refs { + self.add_item_ref_surface(item_ref, Some(qualifier)); + } + self.add_private_item_surfaces(qualifier, target, &interface); + + if !stack.insert(target) { + tracing::trace!( + target: "nameres::imports", + module = %self.module.display(self.db), + qualifier, + target = %target.display(self.db), + "stopped recursive module surface" + ); + return; + } + for (alias, nested) in interface.module_aliases { + let nested_qualifier = qualify(qualifier, &alias); + self.add_module_surface(&nested_qualifier, nested, span, seen, stack); + } + stack.remove(&target); + } + + fn add_private_item_surfaces( + &mut self, + qualifier: &str, + target: ModuleId<'db>, + interface: &Interface<'db>, + ) { + if module_has_parse_errors(self.db, target) { + return; + } + let Some(file) = self.db.module_file(target) else { + return; + }; + let hir_module = parse_file_to_hir(self.db, file).module(self.db); + let item_scope = hir_nameres::item_scope(self.db, hir_module); + let module = module_id_display(self.db, target); + + for entry in &item_scope.terms { + if interface.terms.contains_key(&entry.name) { + continue; + } + self.insert_private_surface( + hir_nameres::Namespace::Term, + qualifier, + &entry.name, + &module, + entry.span, + ); + } + + for entry in &item_scope.types { + if interface.types.contains_key(&entry.name) + || interface.classes.contains_key(&entry.name) + { + continue; + } + self.insert_private_surface( + hir_nameres::Namespace::Type, + qualifier, + &entry.name, + &module, + entry.span, + ); + } + } + + fn insert_private_surface( + &mut self, + namespace: hir_nameres::Namespace, + qualifier: &str, + name: &str, + module: &str, + span: Span<'db>, + ) { + let key = private_surface_key(namespace, qualifier, name); + self.env + .private_surfaces + .entry(key) + .or_insert_with(|| hir_nameres::PrivateCandidate { + name: name.to_owned(), + module: module.to_owned(), + span: LabelSpan::from_span(self.db, span), + }); + } + + fn add_module_binding(&mut self, name: &str, target: ModuleId<'db>, span: Span<'db>) { + for prefix in module_prefixes(name) { + self.env.module_qualifiers.insert(prefix.clone()); + self.env + .module_origins + .entry(prefix.clone()) + .and_modify(|origin| { + if *origin != Some(target) { + *origin = None; + } + }) + .or_insert(Some(target)); + self.module_binding_spans + .entry(prefix.clone()) + .or_insert(span); + } + self.env.modules.entry(name.to_owned()).or_insert(target); + if module_has_parse_errors(self.db, target) { + self.env.incomplete_modules.insert(name.to_owned()); + } + } + + fn check_module_name_conflict(&mut self, name: &str, span: Span<'db>) { + let local_span = self + .local_terms + .get(name) + .copied() + .or_else(|| self.imported_terms.get(name).copied()); + if let Some(local_span) = local_span + && self.module_conflict_diagnostics.insert(name.to_owned()) + { + self.env.diagnostics.push(conflicting_unqualified_name_diag( + self.db, span, local_span, name, + )); + } + } + + fn add_item_ref_surface(&mut self, item_ref: &ItemRef<'db>, qualifier: Option<&str>) { + let name = qualified_surface_name(qualifier, &item_ref.public_name); + match item_ref.namespace { + Namespace::Term => { + if let Some(resolution) = resolution_for_item_ref(self.db, item_ref) { + self.insert_term(name, resolution); + } + } + Namespace::Type => { + if let Some(resolution) = resolution_for_item_ref(self.db, item_ref) { + self.env.types.entry(name.clone()).or_insert(resolution); + } + self.add_constructor_surface(item_ref, &name); + self.add_library_member_surface(item_ref, &name); + } + Namespace::Class => { + if let Some(resolution) = resolution_for_item_ref(self.db, item_ref) { + self.env.types.entry(name.clone()).or_insert(resolution); + } + self.add_class_method_surface(item_ref, &name); + } + } + } + + fn add_library_member_surface(&mut self, item_ref: &ItemRef<'db>, library_name: &str) { + let Some(file) = self.db.module_file(item_ref.origin.module) else { + return; + }; + let hir_module = parse_file_to_hir(self.db, file).module(self.db); + let Some(library) = hir_module + .items(self.db) + .iter() + .find_map(|item| match item { + Item::ContractDef(def) + if def.def_id_value(self.db) == item_ref.origin.def_id + && def.kind(self.db) == ContractKind::Library => + { + Some(*def) + } + _ => None, + }) + else { + return; + }; + let private_functions = library + .items(self.db) + .iter() + .filter_map(|item| match item { + ContractItem::FunctionDef(function) + if function.sig(self.db).visibility_kind() + == Some(FunctionVisibility::Private) => + { + Some(function.def_id_value(self.db)) + } + _ => None, + }) + .collect::>(); + let item_scope = hir_nameres::item_scope(self.db, hir_module); + let Some(library_scope) = item_scope.contract_scope(item_ref.origin.def_id) else { + return; + }; + for entry in &library_scope.types { + self.env + .types + .entry(qualify(library_name, &entry.name)) + .or_insert(entry.resolution.clone()); + } + for entry in &library_scope.terms { + if matches!( + entry.resolution, + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } if private_functions.contains(&def) + ) { + continue; + } + self.insert_term(qualify(library_name, &entry.name), entry.resolution.clone()); + } + } + + fn add_constructor_surface(&mut self, item_ref: &ItemRef<'db>, type_name: &str) { + let visible = match &item_ref.constructors { + ConstructorVisibility::NotData => return, + ConstructorVisibility::OpaqueData => None, + ConstructorVisibility::Visible(constructors) => Some(constructors), + }; + let all = constructor_entries_for_ref(self.db, item_ref); + let all_names = all + .iter() + .map(|(name, _)| name.clone()) + .collect::>(); + let constructor_visibility = self + .env + .constructor_visibility + .entry(type_name.to_owned()) + .or_default(); + if let Some(visible) = visible { + constructor_visibility.extend(visible.iter().cloned()); + } + let has_partial_visibility = visible.map_or(!all_names.is_empty(), |visible| { + visible.as_set() != &all_names + }); + if has_partial_visibility { + let partial_data = self + .env + .partial_data + .entry(type_name.to_owned()) + .or_default(); + if let Some(visible) = visible { + partial_data.extend(visible.iter().cloned()); + } + } + for (ctor_name, index) in all { + if !visible.is_some_and(|visible| visible.contains(&ctor_name)) { + continue; + } + self.env.constructor_leaves.insert(ctor_name.clone()); + self.insert_term( + qualify(type_name, &ctor_name), + hir_nameres::Resolution::Ctor { + ty: item_ref.origin.def_id, + index, + }, + ); + } + } + + fn add_class_method_surface(&mut self, item_ref: &ItemRef<'db>, class_name: &str) { + for method in class_methods_for_ref(self.db, item_ref) { + self.insert_term( + qualify(class_name, &method), + hir_nameres::Resolution::ClassMethod { + class: item_ref.origin.def_id, + name: method, + }, + ); + } + } + + fn insert_term(&mut self, name: String, resolution: hir_nameres::Resolution<'db>) { + self.env.terms.entry(name).or_insert(resolution); + } +} diff --git a/crates/nameres/src/graph.rs b/crates/nameres/src/graph.rs new file mode 100644 index 00000000..6428e233 --- /dev/null +++ b/crates/nameres/src/graph.rs @@ -0,0 +1,131 @@ +use super::*; + +/// Extracts import and export module references from a source file. +/// +/// The parser/lowerer owns syntax diagnostics; this query only classifies the +/// lowered import/export items for graph construction. +#[salsa::tracked] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, file), + fields(file = field::Empty) +)] +pub fn module_imports<'db>(db: &'db dyn Db, file: SourceFile) -> ModuleImports<'db> { + record_source_file_field(db, file); + let module = parse_file_to_hir(db, file).module(db); + let mut imports = Vec::new(); + let mut exports = Vec::new(); + let mut import_refs = Vec::new(); + let mut export_refs = Vec::new(); + + for item in module.items(db) { + match item { + Item::Import(import) => { + imports.push(*import); + import_refs.push(path_ref_from_import(db, *import)); + } + Item::Export(export) => { + exports.push(*export); + export_refs.extend(path_refs_from_export(db, *export)); + } + _ => {} + } + } + + ModuleImports { + imports, + exports, + import_refs, + export_refs, + } +} + +/// Builds the import/export reachability graph from `entry`. +/// +/// Import edges represent direct imports. Reference edges include both imports +/// and module references that appear in exports/re-exports, because those also +/// participate in public-interface cycles. +#[salsa::tracked] +pub fn module_graph<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> ModuleGraph<'db> { + let reachable = collect_reachable_modules(db, entry); + ModuleGraph { + entry, + modules: reachable.modules, + import_edges: reachable.import_edges, + reference_edges: reachable.reference_edges, + } +} + +/// Returns modules reachable from `entry` in the same traversal order as +/// [`module_graph`]. +#[salsa::tracked] +pub fn reachable_modules<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> Vec> { + collect_reachable_modules(db, entry).modules +} + +struct ReachableModules<'db> { + modules: Vec>, + import_edges: Vec>, + reference_edges: Vec>, +} + +fn collect_reachable_modules<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> ReachableModules<'db> { + let mut modules = Vec::new(); + let mut seen = FxHashSet::default(); + let mut queue = VecDeque::from([entry]); + let mut import_edges = Vec::new(); + let mut reference_edges = Vec::new(); + + while let Some(module) = queue.pop_front() { + if !seen.insert(module) { + continue; + } + modules.push(module); + + let Some(file) = db.module_file(module) else { + continue; + }; + let refs = module_imports(db, file); + + for path in refs.import_refs { + if let Ok(target) = resolve_module_path(db, module, path) { + import_edges.push(ModuleEdge { + from: module, + to: target, + }); + reference_edges.push(ModuleEdge { + from: module, + to: target, + }); + queue.push_back(target); + } + } + + for path in refs.export_refs { + if let Ok(target) = resolve_module_path(db, module, path) { + reference_edges.push(ModuleEdge { + from: module, + to: target, + }); + queue.push_back(target); + } + } + } + + ReachableModules { + modules, + import_edges, + reference_edges, + } +} + +/// Runs full resolution for every module reachable from `entry`. +#[salsa::tracked] +pub fn resolve_reachable_full<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> ModuleGraph<'db> { + let graph = module_graph(db, entry); + for module in &graph.modules { + let _ = resolve_module_full(db, *module); + } + graph +} diff --git a/crates/nameres/src/instances.rs b/crates/nameres/src/instances.rs new file mode 100644 index 00000000..eef2527c --- /dev/null +++ b/crates/nameres/src/instances.rs @@ -0,0 +1,128 @@ +use super::*; + +/// Collects instances declared directly in `module`. +/// +/// Missing source files yield an empty list; module loading diagnostics are +/// emitted by graph construction. +#[salsa::tracked] +pub fn module_instances<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> Vec> { + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + let hir_module = parse_file_to_hir(db, file).module(db); + hir_module + .items(db) + .iter() + .filter_map(|item| match item { + Item::InstanceDef(def) => Some(Origin { + module, + def_id: def.def_id(db), + }), + _ => None, + }) + .collect() +} + +/// Collects local and import-chain instance origins for `module`. +#[salsa::tracked] +pub fn instance_imports<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> InstanceImports<'db> { + let local = module_instances(db, module); + let mut imported = Vec::new(); + let mut seen = FxHashSet::default(); + seen.insert(module); + collect_imported_instances(db, module, &mut seen, &mut imported); + imported = unique_origins(imported); + InstanceImports { local, imported } +} + +/// Collects import-chain instance origins using imports parsed from `file`. +/// +/// This is useful for synthetic HIR modules that share a logical `ModuleId` +/// with a file-backed module but use a different effective import list. +pub fn instance_imports_for_file<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + file: SourceFile, +) -> InstanceImports<'db> { + let mut imported = Vec::new(); + let mut seen = FxHashSet::default(); + seen.insert(module); + collect_imported_instances_from_file(db, module, file, &mut seen, &mut imported); + InstanceImports { + local: Vec::new(), + imported: unique_origins(imported), + } +} + +/// Collects local and import-chain instance origins from an effective HIR +/// module. +pub fn instance_imports_for_hir_module<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + hir_module: Module<'db>, +) -> InstanceImports<'db> { + let local = hir_module + .items(db) + .iter() + .filter_map(|item| match item { + Item::InstanceDef(def) => Some(Origin { + module, + def_id: def.def_id(db), + }), + _ => None, + }) + .collect(); + let mut imported = Vec::new(); + let mut seen = FxHashSet::default(); + seen.insert(module); + for import in hir_module.items(db).iter().filter_map(|item| match item { + Item::Import(import) => Some(*import), + _ => None, + }) { + let path = path_ref_from_import(db, import); + let Ok(target) = resolve_module_path(db, module, path) else { + continue; + }; + if !seen.insert(target) { + continue; + } + imported.extend(module_instances(db, target)); + collect_imported_instances(db, target, &mut seen, &mut imported); + } + InstanceImports { + local, + imported: unique_origins(imported), + } +} + +fn collect_imported_instances<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + seen: &mut FxHashSet>, + out: &mut Vec>, +) { + let Some(file) = db.module_file(module) else { + return; + }; + collect_imported_instances_from_file(db, module, file, seen, out); +} + +fn collect_imported_instances_from_file<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + file: SourceFile, + seen: &mut FxHashSet>, + out: &mut Vec>, +) { + let refs = module_imports(db, file); + for path in refs.import_refs { + let Ok(target) = resolve_module_path(db, module, path) else { + continue; + }; + if !seen.insert(target) { + continue; + } + out.extend(module_instances(db, target)); + collect_imported_instances(db, target, seen, out); + } +} diff --git a/crates/nameres/src/interface.rs b/crates/nameres/src/interface.rs new file mode 100644 index 00000000..68b24de7 --- /dev/null +++ b/crates/nameres/src/interface.rs @@ -0,0 +1,510 @@ +use super::*; + +#[derive(Default)] +pub(super) struct RawInterface<'db> { + pub(super) item_refs: Vec>, + pub(super) module_aliases: Vec>, +} + +pub(super) struct RawItemRef<'db> { + pub(super) item_ref: ItemRef<'db>, + pub(super) export_span: Option>, +} + +pub(super) struct RawModuleAlias<'db> { + pub(super) alias: ModuleAlias<'db>, + pub(super) export_span: Option>, +} + +impl<'db> RawInterface<'db> { + fn push_item_ref(&mut self, item_ref: ItemRef<'db>, export_span: Option>) { + self.item_refs.push(RawItemRef { + item_ref, + export_span, + }); + } + + fn extend_item_refs( + &mut self, + item_refs: impl IntoIterator>, + export_span: Option>, + ) { + self.item_refs + .extend(item_refs.into_iter().map(|item_ref| RawItemRef { + item_ref, + export_span, + })); + } + + fn push_module_alias(&mut self, alias: ModuleAlias<'db>, export_span: Option>) { + self.module_aliases + .push(RawModuleAlias { alias, export_span }); + } +} + +/// Computes the public interface exported by `module`. +/// +/// This query may recursively depend on other public interfaces through +/// re-exports. Salsa handles cycles by starting from an empty interface and +/// re-running until interface equality stabilizes; diagnostics that require the +/// final fixed point are emitted by [`validate_module`]. +#[salsa::tracked(cycle_fn = public_interface_cycle, cycle_initial = public_interface_initial)] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, module), + fields(module = field::Empty, file = field::Empty) +)] +pub fn public_interface<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> Interface<'db> { + record_module_field(db, module); + // This query is intentionally side-effect free: during salsa fixed-point + // iteration dependencies in the same recursive module group may still have + // provisional empty interfaces. Strict unknown-name diagnostics are emitted + // by `validate_module` after the cycle has converged. + let mut diagnostics = Vec::new(); + interface_from_raw(expand_module_exports( + db, + module, + ExportResolutionMode::Lenient, + &mut diagnostics, + )) +} + +fn public_interface_initial<'db>( + db: &'db dyn Db, + _id: salsa::Id, + module: ModuleId<'db>, +) -> Interface<'db> { + // Empty is the least assumption for export cycles: no imported name is + // visible until a later iteration can prove it from a concrete interface. + tracing::debug!( + target: "nameres::fixpoint", + module = %module.display(db), + "public interface fixed-point initial value" + ); + Interface::default() +} + +fn public_interface_cycle<'db>( + db: &'db dyn Db, + _cycle: &salsa::Cycle, + last_provisional_value: &Interface<'db>, + value: Interface<'db>, + module: ModuleId<'db>, +) -> Interface<'db> { + // Salsa compares this returned value with the last provisional interface and + // continues the cycle only while it changes. + tracing::debug!( + target: "nameres::fixpoint", + module = %module.display(db), + changed = last_provisional_value != &value, + items = value.item_refs.len(), + module_aliases = value.module_aliases.len(), + "public interface fixed-point iteration" + ); + value +} + +pub(super) fn expand_module_exports<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + mode: ExportResolutionMode, + diagnostics: &mut Vec>, +) -> RawInterface<'db> { + let Some(file) = db.module_file(module) else { + return RawInterface::default(); + }; + let module_items = module_imports(db, file); + if module_items.exports.is_empty() { + return RawInterface::default(); + } + + let mut raw = RawInterface::default(); + let selected_imports = selected_imported_refs(db, module, mode, diagnostics); + for export in module_items.exports { + expand_export( + db, + module, + export, + &selected_imports, + mode, + diagnostics, + &mut raw, + ); + } + raw +} + +fn expand_export<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + export: Export<'db>, + selected_imports: &[ItemRef<'db>], + mode: ExportResolutionMode, + diagnostics: &mut Vec>, + raw: &mut RawInterface<'db>, +) { + match export.kind(db) { + ExportKind::List(names) => { + for name in names { + expand_exported_name(db, module, name, selected_imports, mode, diagnostics, raw); + } + } + ExportKind::Module(path) => { + let path_ref = path_ref_from_segments(db, export.span(db), path.clone()); + if let Some(target) = resolve_for_export(db, module, &path_ref, mode, diagnostics) { + let span = path_ref + .segments + .last() + .map(|segment| segment.span(db)) + .unwrap_or(export.span(db)); + raw.push_module_alias( + ModuleAlias { + public_name: default_module_binding_name(db, &path_ref), + target, + }, + Some(span), + ); + } + } + ExportKind::ModuleAs(path, alias) => { + let path_ref = path_ref_from_segments(db, export.span(db), path.clone()); + if let Some(target) = resolve_for_export(db, module, &path_ref, mode, diagnostics) { + raw.push_module_alias( + ModuleAlias { + public_name: spanned_name_text(db, alias), + target, + }, + Some(alias.span(db)), + ); + } + } + ExportKind::ItemsFrom(path, names) => { + let path_ref = path_ref_from_segments(db, export.span(db), path.clone()); + expand_reexport_items(db, module, &path_ref, names, mode, diagnostics, raw); + } + } +} + +fn expand_exported_name<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + name: &ExportedName<'db>, + selected_imports: &[ItemRef<'db>], + mode: ExportResolutionMode, + diagnostics: &mut Vec>, + raw: &mut RawInterface<'db>, +) { + let text = spanned_name_text(db, &name.name); + let export_span = Some(name.name.span(db)); + if text == "*" { + raw.extend_item_refs(local_importable_refs(db, module), export_span); + return; + } + if let Some(module_text) = text.strip_suffix(".*") { + let path_ref = path_ref_from_text(db, name.name.span(db), module_text); + expand_reexport_items( + db, + module, + &path_ref, + &[ExportedName { + name: SpannedElem::new(Ident::new(db, "*".to_owned()), name.name.span(db)), + constructors: None, + is_operator: false, + }], + mode, + diagnostics, + raw, + ); + return; + } + + match &name.constructors { + Some(selector) => { + let may_be_unknown = selected_import_may_be_unknown(db, module, &text); + let diagnostic_mode = mode.suppress_if(may_be_unknown); + let refs = local_data_ref_with_constructors( + db, + module, + &text, + selector, + mode, + diagnostics, + name, + ) + .or_else(|| { + visible_data_ref_with_constructors( + db, + &text, + selector, + selected_imports, + name, + ConstructorDiagnosticCtx { + mode: diagnostic_mode, + diagnostics, + diagnostic: ConstructorDiagnostic::Local, + }, + ) + }); + if let Some(item_ref) = refs { + raw.push_item_ref(item_ref, export_span); + } else if diagnostic_mode.is_strict() { + diagnostics.push(unknown_local_export_diag(db, name.name.span(db), &text)); + } + } + None => { + let mut refs = local_refs_for_name(db, module, &text); + refs.extend( + selected_imports + .iter() + .filter(|item_ref| item_ref.public_name == text) + .cloned(), + ); + if refs.is_empty() { + if mode + .suppress_if(selected_import_may_be_unknown(db, module, &text)) + .is_strict() + { + diagnostics.push(unknown_local_export_diag(db, name.name.span(db), &text)); + } + } else { + raw.extend_item_refs( + refs.into_iter().map(strip_constructor_visibility), + export_span, + ); + } + } + } +} + +fn selected_import_may_be_unknown<'db>(db: &'db dyn Db, module: ModuleId<'db>, name: &str) -> bool { + let Some(file) = db.module_file(module) else { + return false; + }; + let module_items = module_imports(db, file); + for import in module_items.imports { + let Some(selector) = import.selector(db) else { + continue; + }; + let path = path_ref_from_import(db, import); + let mut scratch = Vec::new(); + let Some(target) = resolve_for_export( + db, + module, + &path, + ExportResolutionMode::Lenient, + &mut scratch, + ) else { + continue; + }; + if !module_has_parse_errors(db, target) { + continue; + } + match selector { + ImportSelector::Wildcard => return true, + ImportSelector::Names(names) => { + if names.iter().any(|selected| { + selected + .alias + .as_ref() + .map(|alias| spanned_name_text(db, alias)) + .unwrap_or_else(|| spanned_name_text(db, &selected.name)) + == name + }) { + return true; + } + } + } + } + false +} + +fn expand_reexport_items<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + path: &ModulePathRef<'db>, + names: &[ExportedName<'db>], + mode: ExportResolutionMode, + diagnostics: &mut Vec>, + raw: &mut RawInterface<'db>, +) { + let Some(target) = resolve_for_export(db, module, path, mode, diagnostics) else { + return; + }; + let interface = public_interface(db, target); + let target_has_parse_errors = module_has_parse_errors(db, target); + let diagnostic_mode = mode.suppress_if(target_has_parse_errors); + + for name in names { + let text = spanned_name_text(db, &name.name); + let export_span = Some(name.name.span(db)); + if text == "*" { + raw.extend_item_refs(interface.item_refs.iter().cloned(), export_span); + continue; + } + + match &name.constructors { + Some(selector) => match visible_data_ref_with_constructors( + db, + &text, + selector, + &interface.item_refs, + name, + ConstructorDiagnosticCtx { + mode: diagnostic_mode, + diagnostics, + diagnostic: ConstructorDiagnostic::ReExport, + }, + ) { + Some(item_ref) => raw.push_item_ref(item_ref, export_span), + None if diagnostic_mode.is_strict() => { + diagnostics.push(unknown_reexport_diag(db, name.name.span(db), &text)); + } + None => {} + }, + None => { + let matching: Vec<_> = interface + .item_refs + .iter() + .filter(|item_ref| item_ref.public_name == text) + .cloned() + .map(strip_constructor_visibility) + .collect(); + if matching.is_empty() { + if diagnostic_mode.is_strict() { + diagnostics.push(unknown_reexport_diag(db, name.name.span(db), &text)); + } + } else { + raw.extend_item_refs(matching, export_span); + } + } + } + } +} + +pub(super) fn resolve_for_export<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + path: &ModulePathRef<'db>, + mode: ExportResolutionMode, + diagnostics: &mut Vec>, +) -> Option> { + match resolve_module_path(db, module, path.clone()) { + Ok(target) => Some(target), + Err(diagnostic) => { + if mode.is_strict() { + diagnostics.push(*diagnostic); + } + None + } + } +} + +fn interface_from_raw<'db>(raw: RawInterface<'db>) -> Interface<'db> { + let mut interface = Interface::default(); + let item_refs = raw.item_refs.into_iter().map(|raw| raw.item_ref).collect(); + for item_ref in normalize_item_refs(item_refs) { + match item_ref.namespace { + Namespace::Term => { + interface + .terms + .entry(item_ref.public_name.clone()) + .or_insert_with(|| item_ref.origin.clone()); + } + Namespace::Type => { + interface + .types + .entry(item_ref.public_name.clone()) + .or_insert_with(|| item_ref.origin.clone()); + match &item_ref.constructors { + ConstructorVisibility::NotData => {} + ConstructorVisibility::OpaqueData => { + interface + .constructor_visibility + .entry(item_ref.public_name.clone()) + .or_default(); + } + ConstructorVisibility::Visible(constructors) => { + interface + .constructor_visibility + .entry(item_ref.public_name.clone()) + .or_default() + .extend(constructors.iter().cloned()); + } + } + } + Namespace::Class => { + interface + .classes + .entry(item_ref.public_name.clone()) + .or_insert_with(|| item_ref.origin.clone()); + } + } + interface.item_refs.push(item_ref); + } + + for raw_alias in raw.module_aliases { + let alias = raw_alias.alias; + interface + .module_aliases + .entry(alias.public_name) + .or_insert(alias.target); + } + interface +} + +fn normalize_item_refs<'db>(refs: Vec>) -> Vec> { + let mut merged: Vec> = Vec::new(); + for item_ref in refs { + if let Some(existing) = merged.iter_mut().find(|existing| { + existing.namespace == item_ref.namespace + && existing.public_name == item_ref.public_name + && existing.source_name == item_ref.source_name + && existing.origin == item_ref.origin + && existing.constructors.is_data() == item_ref.constructors.is_data() + }) { + merge_constructor_visibility(&mut existing.constructors, item_ref.constructors); + } else { + merged.push(item_ref); + } + } + merged.sort_by(|a, b| { + ( + namespace_sort_key(a.namespace), + &a.public_name, + &a.source_name, + ) + .cmp(&( + namespace_sort_key(b.namespace), + &b.public_name, + &b.source_name, + )) + }); + merged +} + +fn merge_constructor_visibility(existing: &mut ConstructorVisibility, new: ConstructorVisibility) { + match (existing, new) { + (ConstructorVisibility::Visible(existing), ConstructorVisibility::Visible(new)) => { + existing.extend(new); + } + (existing @ ConstructorVisibility::OpaqueData, ConstructorVisibility::Visible(new)) => { + *existing = ConstructorVisibility::from_visible(new.into_names()); + } + (ConstructorVisibility::Visible(_), ConstructorVisibility::OpaqueData) + | (ConstructorVisibility::OpaqueData, ConstructorVisibility::OpaqueData) + | (ConstructorVisibility::NotData, ConstructorVisibility::NotData) => {} + (ConstructorVisibility::NotData, ConstructorVisibility::OpaqueData) + | (ConstructorVisibility::NotData, ConstructorVisibility::Visible(_)) + | (ConstructorVisibility::OpaqueData, ConstructorVisibility::NotData) + | (ConstructorVisibility::Visible(_), ConstructorVisibility::NotData) => {} + } +} + +pub(super) fn namespace_sort_key(namespace: Namespace) -> u8 { + match namespace { + Namespace::Term => 0, + Namespace::Type => 1, + Namespace::Class => 2, + } +} diff --git a/crates/nameres/src/item_refs.rs b/crates/nameres/src/item_refs.rs new file mode 100644 index 00000000..cd6914d0 --- /dev/null +++ b/crates/nameres/src/item_refs.rs @@ -0,0 +1,629 @@ +use super::*; + +pub(super) fn path_ref_from_import<'db>( + db: &'db dyn Db, + import: Import<'db>, +) -> ModulePathRef<'db> { + let mut path = ModulePathRef { + span: import.span(db), + external: import.external(db), + segments: import.path(db).clone(), + }; + path.span = module_path_span(db, &path); + path +} + +pub(super) fn path_refs_from_export<'db>( + db: &'db dyn Db, + export: Export<'db>, +) -> Vec> { + match export.kind(db) { + ExportKind::List(names) => names + .iter() + .filter_map(|name| module_wildcard_path_ref(db, &name.name)) + .collect(), + ExportKind::Module(path) | ExportKind::ItemsFrom(path, _) => { + vec![path_ref_from_segments(db, export.span(db), path.clone())] + } + ExportKind::ModuleAs(path, _) => { + vec![path_ref_from_segments(db, export.span(db), path.clone())] + } + } +} + +fn module_wildcard_path_ref<'db>( + db: &'db dyn Db, + name: &SpannedElem<'db, Ident<'db>>, +) -> Option> { + let text = spanned_name_text(db, name); + let prefix = text.strip_suffix(".*")?; + if prefix.is_empty() { + return None; + } + Some(path_ref_from_text(db, name.span(db), prefix)) +} + +pub(super) fn path_ref_from_segments<'db>( + _db: &'db dyn Db, + span: Span<'db>, + segments: Vec>>, +) -> ModulePathRef<'db> { + ModulePathRef { + span, + external: None, + segments, + } +} + +pub(super) fn path_ref_from_text<'db>( + db: &'db dyn Db, + span: Span<'db>, + text: &str, +) -> ModulePathRef<'db> { + let segments = text + .split('.') + .filter(|segment| !segment.is_empty()) + .map(|segment| SpannedElem::new(Ident::new(db, segment.to_owned()), span)) + .collect(); + ModulePathRef { + span, + external: None, + segments, + } +} + +pub(super) fn local_importable_refs<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, +) -> Vec> { + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + let hir_module = parse_file_to_hir(db, file).module(db); + let mut refs = Vec::new(); + for item in hir_module.items(db) { + refs.extend(local_refs_for_item( + db, + module, + item, + CtorInclusion::Exclude, + )); + } + refs +} + +pub(super) fn local_refs_for_name<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + name: &str, +) -> Vec> { + local_importable_refs(db, module) + .into_iter() + .filter(|item_ref| item_ref.public_name == name) + .collect() +} + +fn local_refs_for_item<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + item: &Item<'db>, + ctor_inclusion: CtorInclusion, +) -> Vec> { + match item { + Item::FunctionDef(def) => vec![function_ref(db, module, *def)], + Item::TypeAlias(def) => vec![type_alias_ref(db, module, *def)], + Item::AdtDef(def) => vec![adt_ref(db, module, *def, ctor_inclusion)], + Item::ClassDef(def) => vec![class_ref(db, module, *def)], + Item::ContractDef(def) => vec![contract_ref(db, module, *def)], + Item::InstanceDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => Vec::new(), + } +} + +fn function_ref<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def: FunctionDef<'db>, +) -> ItemRef<'db> { + let name = spanned_name_text(db, &def.sig(db).name); + ItemRef { + namespace: Namespace::Term, + public_name: name.clone(), + source_name: name, + origin: Origin { + module, + def_id: def.def_id(db), + }, + constructors: ConstructorVisibility::NotData, + } +} + +fn type_alias_ref<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def: TypeAlias<'db>, +) -> ItemRef<'db> { + let name = spanned_name_text(db, &def.name(db)); + ItemRef { + namespace: Namespace::Type, + public_name: name.clone(), + source_name: name, + origin: Origin { + module, + def_id: def.def_id(db), + }, + constructors: ConstructorVisibility::NotData, + } +} + +fn adt_ref<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def: AdtDef<'db>, + ctor_inclusion: CtorInclusion, +) -> ItemRef<'db> { + let name = spanned_name_text(db, &def.name(db)); + let constructors = if ctor_inclusion.includes_data_ctors() { + ConstructorVisibility::from_visible(ctor_names(db, def).into_iter().collect()) + } else { + ConstructorVisibility::OpaqueData + }; + ItemRef { + namespace: Namespace::Type, + public_name: name.clone(), + source_name: name, + origin: Origin { + module, + def_id: def.def_id(db), + }, + constructors, + } +} + +fn class_ref<'db>(db: &'db dyn Db, module: ModuleId<'db>, def: ClassDef<'db>) -> ItemRef<'db> { + let name = spanned_name_text(db, &def.head(db).kind(db).class); + ItemRef { + namespace: Namespace::Class, + public_name: name.clone(), + source_name: name, + origin: Origin { + module, + def_id: def.def_id(db), + }, + constructors: ConstructorVisibility::NotData, + } +} + +fn contract_ref<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def: ContractDef<'db>, +) -> ItemRef<'db> { + let name = spanned_name_text(db, &def.name(db)); + ItemRef { + namespace: Namespace::Type, + public_name: name.clone(), + source_name: name, + origin: Origin { + module, + def_id: def.def_id(db), + }, + constructors: ConstructorVisibility::NotData, + } +} + +pub(super) fn local_data_ref_with_constructors<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + type_name: &str, + selector: &ConstructorSelector<'db>, + mode: ExportResolutionMode, + diagnostics: &mut Vec>, + exported: &ExportedName<'db>, +) -> Option> { + let def = find_local_data_type(db, module, type_name)?; + let available = ctor_names(db, def); + let selected = select_constructors(db, selector, available.iter().cloned(), |name| { + available.iter().any(|available| available.as_str() == name) + }); + let missing = missing_constructors(db, selector, |name| { + available.iter().any(|available| available.as_str() == name) + }); + if mode.is_strict() { + for ctor in missing { + diagnostics.push(unknown_local_ctor_diag( + db, + exported.name.span(db), + type_name, + &ctor, + )); + } + } + let mut item_ref = adt_ref(db, module, def, CtorInclusion::Exclude); + item_ref.constructors = ConstructorVisibility::from_visible(selected); + Some(item_ref) +} + +pub(super) fn visible_data_ref_with_constructors<'db>( + db: &'db dyn Db, + type_name: &str, + selector: &ConstructorSelector<'db>, + refs: &[ItemRef<'db>], + exported: &ExportedName<'db>, + ctx: ConstructorDiagnosticCtx<'_, 'db>, +) -> Option> { + let data_ref = refs + .iter() + .find(|item_ref| { + item_ref.namespace == Namespace::Type + && item_ref.public_name == type_name + && item_ref.constructors.is_data() + })? + .clone(); + let visible = visible_constructor_set(&data_ref.constructors); + let missing = missing_constructors(db, selector, |name| { + visible.is_some_and(|visible| visible.contains(name)) + }); + if ctx.mode.is_strict() { + for ctor in missing { + ctx.diagnostics.push(match ctx.diagnostic { + ConstructorDiagnostic::Local => { + unknown_local_ctor_diag(db, exported.name.span(db), type_name, &ctor) + } + ConstructorDiagnostic::ReExport => { + unknown_reexport_ctor_diag(db, exported.name.span(db), type_name, &ctor) + } + }); + } + } + let selected_constructors = select_constructors( + db, + selector, + visible + .into_iter() + .flat_map(|visible| visible.iter().cloned()), + |name| visible.is_some_and(|visible| visible.contains(name)), + ); + let mut selected = data_ref; + selected.constructors = ConstructorVisibility::from_visible(selected_constructors); + Some(selected) +} + +#[derive(Clone, Copy)] +pub(super) enum ConstructorDiagnostic { + Local, + ReExport, +} + +pub(super) struct ConstructorDiagnosticCtx<'a, 'db> { + pub(super) mode: ExportResolutionMode, + pub(super) diagnostics: &'a mut Vec>, + pub(super) diagnostic: ConstructorDiagnostic, +} + +fn find_local_data_type<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + type_name: &str, +) -> Option> { + let file = db.module_file(module)?; + let hir_module = parse_file_to_hir(db, file).module(db); + hir_module.items(db).iter().find_map(|item| match item { + Item::AdtDef(def) if spanned_name_text(db, &def.name(db)) == type_name => Some(*def), + _ => None, + }) +} + +fn ctor_names<'db>(db: &'db dyn Db, def: AdtDef<'db>) -> Vec { + def.ctors(db) + .iter() + .map(|ctor| spanned_name_text(db, &ctor.name)) + .collect() +} + +fn select_constructors<'db>( + db: &'db dyn Db, + selector: &ConstructorSelector<'db>, + available: impl IntoIterator, + contains: impl Fn(&str) -> bool, +) -> BTreeSet { + match selector { + ConstructorSelector::All => available.into_iter().collect(), + ConstructorSelector::Named(names) => { + let mut seen = FxHashSet::default(); + let mut selected = BTreeSet::new(); + for name in names.iter().map(|name| spanned_name_text(db, name)) { + if seen.insert(name.clone()) && contains(&name) { + selected.insert(name); + } + } + selected + } + } +} + +fn missing_constructors<'db>( + db: &'db dyn Db, + selector: &ConstructorSelector<'db>, + contains: impl Fn(&str) -> bool, +) -> Vec { + match selector { + ConstructorSelector::All => Vec::new(), + ConstructorSelector::Named(names) => { + let mut seen = FxHashSet::default(); + let mut missing = Vec::new(); + for name in names.iter().map(|name| spanned_name_text(db, name)) { + if seen.insert(name.clone()) && !contains(&name) { + missing.push(name); + } + } + missing + } + } +} + +pub(super) fn strip_constructor_visibility<'db>(mut item_ref: ItemRef<'db>) -> ItemRef<'db> { + if item_ref.constructors.is_data() { + item_ref.constructors = ConstructorVisibility::OpaqueData; + } + item_ref +} + +pub(super) fn selected_imported_refs<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + mode: ExportResolutionMode, + diagnostics: &mut Vec>, +) -> Vec> { + let Some(file) = db.module_file(module) else { + return Vec::new(); + }; + let module_items = module_imports(db, file); + let mut refs = Vec::new(); + for import in module_items.imports { + let Some(selector) = import.selector(db) else { + continue; + }; + let path = path_ref_from_import(db, import); + let Some(target) = resolve_for_export(db, module, &path, mode, diagnostics) else { + continue; + }; + let interface = public_interface(db, target); + refs.extend(select_import_refs( + db, + &interface.item_refs, + selector, + import.hiding(db), + )); + } + refs +} + +pub(super) fn select_import_refs<'db>( + db: &'db dyn Db, + available: &[ItemRef<'db>], + selector: &ImportSelector<'db>, + hiding: &[ImportHiddenName<'db>], +) -> Vec> { + let hidden: FxHashSet<_> = hiding + .iter() + .map(|hidden| spanned_name_text(db, &hidden.name)) + .collect(); + let mut selected = match selector { + ImportSelector::Wildcard => available.to_vec(), + ImportSelector::Names(names) => names + .iter() + .flat_map(|selected| { + let source_name = spanned_name_text(db, &selected.name); + let local_name = selected + .alias + .as_ref() + .map(|alias| spanned_name_text(db, alias)) + .unwrap_or_else(|| source_name.clone()); + available + .iter() + .filter(move |item_ref| item_ref.public_name == source_name) + .cloned() + .map(move |mut item_ref| { + item_ref.public_name = local_name.clone(); + if let Some(selector) = &selected.constructors + && item_ref.constructors.is_data() + { + let visible = visible_constructor_set(&item_ref.constructors); + let selected_constructors = select_constructors( + db, + selector, + visible + .into_iter() + .flat_map(|visible| visible.iter().cloned()), + |name| visible.is_some_and(|visible| visible.contains(name)), + ); + item_ref.constructors = + ConstructorVisibility::from_visible(selected_constructors); + } + item_ref + }) + }) + .collect(), + }; + selected.retain(|item_ref| !hidden.contains(&item_ref.public_name)); + let selected = unique_import_bindings(selected); + tracing::trace!( + target: "nameres::imports", + selector = selector_kind(selector), + available = available.len(), + hidden = hidden.len(), + selected = selected.len(), + "filtered import refs" + ); + selected +} + +fn visible_constructor_set(visibility: &ConstructorVisibility) -> Option<&BTreeSet> { + match visibility { + ConstructorVisibility::NotData | ConstructorVisibility::OpaqueData => None, + ConstructorVisibility::Visible(constructors) => Some(constructors.as_set()), + } +} + +fn unique_import_bindings<'db>(refs: Vec>) -> Vec> { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for item_ref in refs { + if seen.insert((item_ref.namespace, item_ref.public_name.clone())) { + result.push(item_ref); + } + } + result +} + +pub(super) fn import_module_qualifiers<'db>( + db: &'db dyn Db, + import: Import<'db>, + path: &ModulePathRef<'db>, +) -> Vec { + if let Some(alias) = import.alias(db) { + return vec![spanned_name_text(db, &alias)]; + } + let visible = visible_module_segments(db, path); + let Some(leaf) = visible.last().cloned() else { + return Vec::new(); + }; + unique_strings([leaf, visible.join(".")]) +} + +fn visible_module_segments<'db>(db: &'db dyn Db, path: &ModulePathRef<'db>) -> Vec { + let segments = path_segments(db, path); + if path.external.is_some() && segments.len() > 1 { + return segments[1..].to_vec(); + } + if segments.first().is_some_and(|segment| segment == "lib") && segments.len() > 1 { + return segments[1..].to_vec(); + } + segments +} + +pub(super) fn module_prefixes(name: &str) -> Vec { + let mut prefixes = Vec::new(); + let mut current = String::new(); + for segment in name.split('.').filter(|segment| !segment.is_empty()) { + if !current.is_empty() { + current.push('.'); + } + current.push_str(segment); + prefixes.push(current.clone()); + } + prefixes +} + +pub(super) fn qualified_surface_name(qualifier: Option<&str>, name: &str) -> String { + qualifier + .map(|qualifier| qualify(qualifier, name)) + .unwrap_or_else(|| name.to_owned()) +} + +pub(super) fn qualify(qualifier: &str, name: &str) -> String { + format!("{qualifier}.{name}") +} + +pub(super) fn resolution_for_item_ref<'db>( + db: &'db dyn Db, + item_ref: &ItemRef<'db>, +) -> Option> { + match item_ref.namespace { + Namespace::Term => Some(hir_nameres::Resolution::Def { + def: item_ref.origin.def_id, + kind: hir_nameres::DefResolutionKind::Function, + }), + Namespace::Type => def_resolution_kind(db, item_ref.origin.def_id).map(|kind| { + hir_nameres::Resolution::Def { + def: item_ref.origin.def_id, + kind, + } + }), + Namespace::Class => Some(hir_nameres::Resolution::Def { + def: item_ref.origin.def_id, + kind: hir_nameres::DefResolutionKind::Class, + }), + } +} + +fn def_resolution_kind<'db>( + db: &'db dyn Db, + def_id: DefId<'db>, +) -> Option { + match def_id.kind(db) { + DefKind::Function => Some(hir_nameres::DefResolutionKind::Function), + DefKind::Contract => Some(hir_nameres::DefResolutionKind::Contract), + DefKind::Adt => Some(hir_nameres::DefResolutionKind::Adt), + DefKind::TypeAlias => Some(hir_nameres::DefResolutionKind::TypeAlias), + DefKind::ValueType => Some(hir_nameres::DefResolutionKind::ValueType), + DefKind::Class => Some(hir_nameres::DefResolutionKind::Class), + DefKind::Instance => Some(hir_nameres::DefResolutionKind::Instance), + DefKind::Module + | DefKind::FuncBody + | DefKind::AdtCtor + | DefKind::Field + | DefKind::Import + | DefKind::Export + | DefKind::Pragma => None, + } +} + +pub(super) fn constructor_entries_for_ref<'db>( + db: &'db dyn Db, + item_ref: &ItemRef<'db>, +) -> Vec<(String, hir_nameres::CtorIndex)> { + let Some(def) = find_origin_adt(db, item_ref.origin.module, item_ref.origin.def_id) else { + return Vec::new(); + }; + def.ctors(db) + .iter() + .enumerate() + .map(|(index, ctor)| { + ( + spanned_name_text(db, &ctor.name), + hir_nameres::CtorIndex::from_usize(index), + ) + }) + .collect() +} + +pub(super) fn class_methods_for_ref<'db>(db: &'db dyn Db, item_ref: &ItemRef<'db>) -> Vec { + let Some(def) = find_origin_class(db, item_ref.origin.module, item_ref.origin.def_id) else { + return Vec::new(); + }; + def.methods(db) + .iter() + .map(|method| spanned_name_text(db, &method.name)) + .collect() +} + +fn find_origin_adt<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def_id: DefId<'db>, +) -> Option> { + let file = db.module_file(module)?; + let hir_module = parse_file_to_hir(db, file).module(db); + hir_module.items(db).iter().find_map(|item| match item { + Item::AdtDef(def) if def.def_id(db) == def_id => Some(*def), + _ => None, + }) +} + +fn find_origin_class<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + def_id: DefId<'db>, +) -> Option> { + let file = db.module_file(module)?; + let hir_module = parse_file_to_hir(db, file).module(db); + hir_module.items(db).iter().find_map(|item| match item { + Item::ClassDef(def) if def.def_id(db) == def_id => Some(*def), + _ => None, + }) +} diff --git a/crates/nameres/src/lib.rs b/crates/nameres/src/lib.rs new file mode 100644 index 00000000..183bb528 --- /dev/null +++ b/crates/nameres/src/lib.rs @@ -0,0 +1,122 @@ +//! Inter-module name resolution and public interface construction. +//! +//! This crate sits above parsing and HIR name resolution. It maps logical +//! module paths to source files, gathers imports/exports, builds a reachable +//! module graph, computes each module's public interface, and finally resolves +//! local HIR bodies with imported names available. +//! +//! [`ModuleId`] is logical, not textual or filesystem identity. It is interned +//! from a [`ModuleKey`] containing the library (`main`, `std`, or an external +//! root) plus the module path inside that library. The same source text reached +//! through a different library root is a different module by design. +//! +//! Public interfaces are Salsa tracked with a fixed point: +//! `public_interface_initial` seeds cyclic queries with an empty interface, and +//! `public_interface_cycle` keeps the newer result only when it changes. +//! Starting empty is conservative: during an import/export cycle, no name is +//! assumed visible until a real expansion proves it. Repeated evaluation grows +//! or stabilizes the interface until the cycle converges. + +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + path::{Path, PathBuf}, +}; + +use hir::{ + anchor::{DefId, DefKind}, + ast::{ + Ident, + function::{FuncBody, FuncParam, FunctionVisibility}, + item::{ + AdtDef, ClassDef, ConstructorSelector, ContractDef, ContractItem, ContractKind, Export, + ExportKind, ExportedName, FunctionDef, Import, ImportHiddenName, ImportSelector, Item, + Module, SelectedName, TypeAlias, + }, + }, + diag::{ + AnchoredTextEdit, AnyDiagnostic, Applicability, Diagnostic, DiagnosticCode, LabelSpan, + Offset, Suggestion, sort_dedup_query_diagnostics, + }, + input::SourceFile, + nameres as hir_nameres, + span::{AnchorId, Span, Spanned, SpannedElem}, +}; +use parser::{parse_diagnostics, parse_file_to_hir}; +use rustc_hash::{FxHashMap, FxHashSet}; +use tracing::{Level, field}; + +mod auto_import; +mod diagnostics; +mod env; +mod graph; +mod instances; +mod interface; +mod item_refs; +mod model; +mod modes; +mod paths; +mod scc; +mod util; +mod validation; + +pub use auto_import::{ + auto_import_candidates, auto_import_constructor_candidates, auto_import_index, + auto_import_module_candidates, source_import_path, +}; +pub use diagnostics::{ + ModuleDiagnostic, body_diagnostics, module_diagnostics, reachable_diagnostics, +}; +use diagnostics::{ + ambiguous_import_diag, conflicting_unqualified_name_diag, duplicate_export_item_diag, + duplicate_export_module_diag, duplicate_qualifier_diag, duplicate_selector_diag, + missing_external_root_diag, module_not_found_diag, module_root_span, unknown_import_item_diag, + unknown_local_ctor_diag, unknown_local_export_diag, unknown_reexport_ctor_diag, + unknown_reexport_diag, +}; +use env::module_has_parse_errors; +pub use env::{module_env, module_env_for_hir_module, module_import_surface, resolve_module_full}; +pub use graph::{module_graph, module_imports, reachable_modules, resolve_reachable_full}; +pub use instances::{ + instance_imports, instance_imports_for_file, instance_imports_for_hir_module, module_instances, +}; +pub use interface::public_interface; +use interface::{ + RawInterface, RawItemRef, RawModuleAlias, expand_module_exports, namespace_sort_key, + resolve_for_export, +}; +use item_refs::{ + ConstructorDiagnostic, ConstructorDiagnosticCtx, class_methods_for_ref, + constructor_entries_for_ref, import_module_qualifiers, local_data_ref_with_constructors, + local_importable_refs, local_refs_for_name, module_prefixes, path_ref_from_import, + path_ref_from_segments, path_ref_from_text, path_refs_from_export, qualified_surface_name, + qualify, resolution_for_item_ref, select_import_refs, selected_imported_refs, + strip_constructor_visibility, visible_data_ref_with_constructors, +}; +pub use model::{ + AutoImportCandidate, AutoImportModuleCandidate, ConstructorVisibility, Db, + FullResolutionSummary, InstanceImports, Interface, ItemRef, LibraryId, ModuleAlias, ModuleEdge, + ModuleEnv, ModuleFileSnapshot, ModuleFsSnapshot, ModuleGraph, ModuleId, ModuleImportSurface, + ModuleImports, ModuleKey, ModulePathRef, ModuleTree, Namespace, Origin, ResolvedModulePath, + ValidationSummary, VisibleConstructors, +}; +use modes::{BodyDiagnosticPolicy, CtorInclusion, ExportResolutionMode}; +use paths::{module_path_span, path_segments}; +pub use paths::{ + resolve_direct_import_target, resolve_direct_import_target_candidate, resolve_module_path, + resolve_module_path_candidate, +}; +pub use scc::strongly_connected_components; +pub use util::{ + ModuleDisplay, ModulePathDisplay, module_file_path, module_id_display, + module_id_for_source_file, module_id_from_key, module_key_for_path, module_path_display, +}; +use util::{ + best_name_suggestion, ident_text, main_workspace_prefix, namespace_context, + private_surface_key, record_body_field, record_module_field, record_source_file_field, + selector_kind, sorted_namespaces, spanned_name_text, trace_import_decision, unique_modules, + unique_origins, unique_strings, +}; +use validation::{ + default_module_binding_name, interface_names, validate_duplicate_exports, validate_imports, +}; +pub use validation::{validate_module, validate_reachable}; diff --git a/crates/nameres/src/model.rs b/crates/nameres/src/model.rs new file mode 100644 index 00000000..3c82a930 --- /dev/null +++ b/crates/nameres/src/model.rs @@ -0,0 +1,603 @@ +use super::*; + +#[salsa::db] +pub trait Db: parser::Db { + /// Returns the logical library roots available to this compilation. + fn module_tree(&self) -> ModuleTree; + + /// Returns the filesystem facts used by module path resolution. + fn module_fs_snapshot(&self) -> ModuleFsSnapshot; + + /// Returns the tracked mapping from logical modules to loaded source files. + fn module_file_snapshot(&self) -> ModuleFileSnapshot; + + /// Returns the source file loaded for a logical module, if any. + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option; +} + +/// Input describing the module roots for a compilation. +/// +/// Paths are expected to be normalized by the driver. External roots are keyed +/// by the library name used after `@` imports. +#[salsa::input(debug)] +pub struct ModuleTree { + /// Root directory for the main input library. + #[returns(ref)] + pub main_root: PathBuf, + + /// Root directory for the standard library. + #[returns(ref)] + pub std_root: PathBuf, + + /// Named external library roots. + #[returns(ref)] + pub external_roots: BTreeMap, +} + +/// Snapshot of module filesystem facts used by tracked module resolution. +/// +/// This input is populated by drivers/tests outside tracked queries. Paths are +/// expected to use the same normalized roots as [`ModuleTree`]. +#[salsa::input(debug)] +pub struct ModuleFsSnapshot { + /// Absolute `.solc` source files observed on disk. + #[returns(ref)] + pub existing_files: BTreeSet, + + /// Sibling `.solc` file stems by parent directory. + #[returns(ref)] + pub sibling_stems: BTreeMap>, +} + +/// Snapshot of the source files loaded for each logical module. +/// +/// The complete mapping is a Salsa input so tracked name-resolution queries do +/// not depend on driver-owned, untracked maps. Editing the contents of an +/// existing [`SourceFile`] leaves this snapshot unchanged; only adding, +/// removing, or remapping a logical module updates it. +#[salsa::input(debug)] +pub struct ModuleFileSnapshot { + /// Loaded source file by lifetime-free logical module identity. + #[returns(ref)] + pub files: BTreeMap, +} + +/// Logical library namespace that owns a module path. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)] +pub enum LibraryId { + /// User input tree. + Main, + /// Standard library tree. + Std, + /// Named external library root. + External(String), +} + +/// Lifetime-free logical module key. +/// +/// This is the driver-facing form of a module identity. It can live in normal +/// maps and be re-interned as a [`ModuleId`] when a database is available. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ModuleKey { + /// Library root that owns the path. + pub library: LibraryId, + /// Dot/path segments relative to the library root. + pub logical_path: Vec, +} + +/// Interned logical module identity. +/// +/// Module identity is based on library plus logical path. Absolute file paths +/// are derived from the module tree and may change without changing the logical +/// module. +#[salsa::interned(debug)] +pub struct ModuleId<'db> { + /// Library root that owns this module. + #[returns(ref)] + pub library: LibraryId, + + /// Dot/path segments relative to the library root. + #[returns(ref)] + pub logical_path: Vec, +} + +impl<'db> ModuleId<'db> { + /// Returns this module's lifetime-free key. + pub fn key(self, db: &'db dyn Db) -> ModuleKey { + ModuleKey { + library: self.library(db).clone(), + logical_path: self.logical_path(db).clone(), + } + } + + /// Returns a borrowed human-readable module path formatter. + pub fn display(self, db: &'db dyn Db) -> ModuleDisplay<'db> { + ModuleDisplay::new(db, self) + } +} + +/// Module path reference extracted from import/export syntax. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModulePathRef<'db> { + /// Span covering the complete module path syntax. + pub span: Span<'db>, + /// Span of the external-library marker when present. + pub external: Option>, + /// Path segments in source order. + pub segments: Vec>>, +} + +/// Import/export module references found in one source file. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleImports<'db> { + /// Import declarations in source order. + pub imports: Vec>, + /// Export declarations in source order. + pub exports: Vec>, + /// Module paths mentioned by imports. + pub import_refs: Vec>, + /// Module paths mentioned by exports/re-exports. + pub export_refs: Vec>, +} + +/// Resolved module path and its file location. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ResolvedModulePath<'db> { + /// Logical module identity. + pub module: ModuleId<'db>, + /// Absolute source file path for the module. + pub file_path: PathBuf, +} + +/// Interface namespace. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum Namespace { + /// Term namespace. + Term, + /// Type namespace. + Type, + /// Class namespace. + Class, +} + +/// Origin of a public/imported item. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct Origin<'db> { + /// Module where the item originates. + pub module: ModuleId<'db>, + /// Definition identity of the originating item. + pub def_id: DefId<'db>, +} + +/// One public symbol that can be brought into a module with a selective import. +/// +/// `provider` is the module named by the generated import while `origin` is the +/// definition ultimately exposed by that provider. Their equality therefore +/// distinguishes direct exports from re-exports without discarding definition +/// identity needed by candidate ranking. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct AutoImportCandidate<'db> { + /// Loaded module whose public interface exposes this symbol. + pub provider: ModuleId<'db>, + /// Canonical source-level path to use in an import declaration. + pub import_path: String, + /// Name exposed by the provider and accepted by a selective import. + pub public_name: String, + /// Namespace in which the name is exported. + pub namespace: Namespace, + /// Definition identity ultimately reached through the provider. + pub origin: Origin<'db>, +} + +impl<'db> AutoImportCandidate<'db> { + /// Returns `true` when the provider exposes a definition from another + /// module rather than one of its own definitions. + pub fn is_reexport(&self) -> bool { + self.provider != self.origin.module + } +} + +/// One module that can be brought into scope under its default qualifier. +/// +/// Unlike [`AutoImportCandidate`], this candidate represents a namespace +/// import (`import * as foo from lib.foo;`). `member` is retained as evidence +/// that the requested immediate qualified term lookup is present in the +/// provider's public interface. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct AutoImportModuleCandidate<'db> { + /// Loaded module named by the generated import. + pub provider: ModuleId<'db>, + /// Canonical source-level path to use in an import declaration. + pub import_path: String, + /// Default leaf qualifier introduced by the import. + pub qualifier: String, + /// Immediate public member that motivated the import. + pub member: String, +} + +/// Public or imported item reference. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ItemRef<'db> { + /// Namespace in which the item is visible. + pub namespace: Namespace, + /// Name exposed by an interface or import. + pub public_name: String, + /// Original name in the source module. + pub source_name: String, + /// Module/definition origin. + pub origin: Origin<'db>, + /// Constructor visibility for data types. + pub constructors: ConstructorVisibility, +} + +/// Constructor visibility carried by an item reference. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub enum ConstructorVisibility { + /// The referenced item is not a data type. + NotData, + /// The referenced item is a data type, but no constructors are visible. + OpaqueData, + /// The referenced item is a data type with these visible constructors. + Visible(VisibleConstructors), +} + +/// Non-empty ordered set of visible constructor names. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct VisibleConstructors { + names: BTreeSet, +} + +impl ConstructorVisibility { + /// Normalizes an empty visible set to opaque data. + pub fn from_visible(constructors: BTreeSet) -> Self { + if constructors.is_empty() { + Self::OpaqueData + } else { + Self::Visible(VisibleConstructors { + names: constructors, + }) + } + } + + /// Returns whether this reference denotes a data type. + pub fn is_data(&self) -> bool { + !matches!(self, Self::NotData) + } +} + +impl VisibleConstructors { + /// Creates a non-empty visible constructor set. + pub fn new(names: BTreeSet) -> Option { + if names.is_empty() { + None + } else { + Some(Self { names }) + } + } + + /// Iterates over constructor names in deterministic order. + pub fn iter(&self) -> impl Iterator { + self.names.iter() + } + + /// Returns whether this set contains `name`. + pub fn contains(&self, name: &str) -> bool { + self.names.contains(name) + } + + /// Returns the underlying ordered set. + pub fn as_set(&self) -> &BTreeSet { + &self.names + } + + /// Extends this set with another non-empty constructor set. + pub fn extend(&mut self, constructors: VisibleConstructors) { + self.names.extend(constructors.names); + } + + /// Consumes this wrapper and returns the underlying ordered set. + pub fn into_names(self) -> BTreeSet { + self.names + } +} + +/// Public module alias exported by an interface. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleAlias<'db> { + /// Alias name visible to importers. + pub public_name: String, + /// Target module identity. + pub target: ModuleId<'db>, +} + +/// Public interface of one module. +/// +/// The maps are the lookup surfaces used by imports and re-exports. `item_refs` +/// preserves normalized item references for selector filtering and constructor +/// visibility. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update)] +pub struct Interface<'db> { + /// Public term names. + pub terms: BTreeMap>, + /// Public type names. + pub types: BTreeMap>, + /// Public class names. + pub classes: BTreeMap>, + /// Public constructors per data type name. + pub constructor_visibility: BTreeMap>, + /// Public module aliases. + pub module_aliases: BTreeMap>, + /// Normalized public item references. + pub item_refs: Vec>, +} + +/// Directed edge in a reachable module graph. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleEdge<'db> { + /// Source module. + pub from: ModuleId<'db>, + /// Target module. + pub to: ModuleId<'db>, +} + +/// Reachable module graph from an entry module. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleGraph<'db> { + /// Entry module. + pub entry: ModuleId<'db>, + /// Reachable modules in traversal order. + pub modules: Vec>, + /// Edges from import declarations. + pub import_edges: Vec>, + /// Edges from export/re-export references. + pub reference_edges: Vec>, +} + +/// Summary returned by validation queries. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ValidationSummary { + /// `true` once validation has traversed the module. + pub checked: bool, +} + +/// Instance origins visible for a module. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct InstanceImports<'db> { + /// Locally declared instances. + pub local: Vec>, + /// Imported instances. + pub imported: Vec>, +} + +/// Facts imported from other modules and supplied to HIR name resolution. +/// +/// This surface intentionally excludes diagnostics. Type lowering, trait-env +/// construction, and body inference should depend on this value rather than on +/// [`ModuleEnv`] so import-diagnostic-only edits can backdate before reaching +/// type queries. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleImportSurface<'db> { + /// Owner used when synthesizing module qualifier resolutions. + pub owner: Option>, + /// Local item-scope facts, when loaded. + pub item_scope: Option>, + /// Imported term names. + pub terms: BTreeMap>, + /// Imported type/class names. + pub types: BTreeMap>, + /// Module qualifiers with an exact target module. + pub modules: BTreeMap>, + /// All visible module qualifiers, including existence-only path prefixes. + pub module_qualifiers: BTreeSet, + /// Direct import target that introduced each visible qualifier, or + /// `None` when imports with different targets share that prefix. + /// + /// A path prefix need not denote a source module of its own, so this map + /// is for binding-origin/navigation queries rather than semantic member + /// lookup. Exact semantic targets remain in [`Self::modules`]. + pub module_origins: BTreeMap>>, + /// Constructor leaf names visible from imported data types. + pub constructor_leaves: BTreeSet, + /// Constructor visibility by public data type name. + pub constructor_visibility: BTreeMap>, + /// Data types imported with only a subset of constructors. + pub partial_data: BTreeMap>, + /// Names selected from parse-broken providers whose namespace is unknown. + pub unknown_unqualified_names: BTreeSet, + /// Whether a wildcard import from a parse-broken provider makes any missing + /// unqualified name potentially part of that incomplete interface. + pub unknown_unqualified_wildcard: bool, + /// Module qualifiers whose target provider had parse errors. + pub incomplete_modules: BTreeSet, + /// Private imported items addressable by qualified module syntax but not + /// exported. + pub private_surfaces: BTreeMap, + /// Instances visible from local and imported modules. + pub instances: Vec>, +} + +/// Imported-name environment supplied to HIR name resolution. +/// +/// This compatibility composite keeps diagnostics together with the import +/// facts for frontend diagnostic aggregation. Facts-only consumers should use +/// [`ModuleImportSurface`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct ModuleEnv<'db> { + /// Facts used by lookup and type inference. + pub surface: ModuleImportSurface<'db>, + /// Local item scope with diagnostics, when loaded. + pub item_scope: Option>, + /// Diagnostics found while building the import environment. + pub diagnostics: Vec>, +} + +impl<'db> ModuleImportSurface<'db> { + pub(super) fn empty() -> Self { + Self { + owner: None, + item_scope: None, + terms: BTreeMap::new(), + types: BTreeMap::new(), + modules: BTreeMap::new(), + module_qualifiers: BTreeSet::new(), + module_origins: BTreeMap::new(), + constructor_leaves: BTreeSet::new(), + constructor_visibility: BTreeMap::new(), + partial_data: BTreeMap::new(), + unknown_unqualified_names: BTreeSet::new(), + unknown_unqualified_wildcard: false, + incomplete_modules: BTreeSet::new(), + private_surfaces: BTreeMap::new(), + instances: Vec::new(), + } + } +} + +impl<'db> ModuleEnv<'db> { + pub(super) fn empty() -> Self { + Self { + surface: ModuleImportSurface::empty(), + item_scope: None, + diagnostics: Vec::new(), + } + } + + /// Returns the import facts without diagnostics. + pub fn import_surface(&self) -> ModuleImportSurface<'db> { + self.surface.clone() + } +} + +impl<'db> std::ops::Deref for ModuleEnv<'db> { + type Target = ModuleImportSurface<'db>; + + fn deref(&self) -> &Self::Target { + &self.surface + } +} + +impl<'db> std::ops::DerefMut for ModuleEnv<'db> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.surface + } +} + +impl<'db> hir_nameres::ImportedNames<'db> for ModuleImportSurface<'db> { + fn imported( + &self, + _db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + name: &str, + ) -> Option> { + match namespace { + hir_nameres::Namespace::Type => self.types.get(name).cloned(), + hir_nameres::Namespace::Term => self.terms.get(name).cloned(), + hir_nameres::Namespace::Module => self.owner.and_then(|owner| { + self.module_qualifiers.contains(name).then(|| { + hir_nameres::Resolution::Module(hir_nameres::ModuleRef { + owner, + name: name.to_owned(), + }) + }) + }), + hir_nameres::Namespace::Field => None, + } + } + + fn has_constructor_leaf(&self, _db: &'db dyn hir::Db, leaf: &str) -> bool { + self.constructor_leaves.contains(leaf) + } + + fn may_contain_unknown_unqualified( + &self, + _db: &'db dyn hir::Db, + _namespace: hir_nameres::Namespace, + name: &str, + ) -> bool { + self.unknown_unqualified_wildcard || self.unknown_unqualified_names.contains(name) + } + + fn has_incomplete_module_qualifier(&self, _db: &'db dyn hir::Db, qualifier: &str) -> bool { + self.incomplete_modules.contains(qualifier) + } + + fn candidate_names( + &self, + _db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + ) -> Vec { + match namespace { + hir_nameres::Namespace::Type => self.types.keys().cloned().collect(), + hir_nameres::Namespace::Term => self.terms.keys().cloned().collect(), + hir_nameres::Namespace::Module => self.module_qualifiers.iter().cloned().collect(), + hir_nameres::Namespace::Field => Vec::new(), + } + } + + fn private_candidate( + &self, + _db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + qualifier: &str, + name: &str, + ) -> Option { + self.private_surfaces + .get(&private_surface_key(namespace, qualifier, name)) + .cloned() + } +} + +impl<'db> hir_nameres::ImportedNames<'db> for ModuleEnv<'db> { + fn imported( + &self, + db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + name: &str, + ) -> Option> { + self.surface.imported(db, namespace, name) + } + + fn has_constructor_leaf(&self, db: &'db dyn hir::Db, leaf: &str) -> bool { + self.surface.has_constructor_leaf(db, leaf) + } + + fn may_contain_unknown_unqualified( + &self, + db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + name: &str, + ) -> bool { + self.surface + .may_contain_unknown_unqualified(db, namespace, name) + } + + fn has_incomplete_module_qualifier(&self, db: &'db dyn hir::Db, qualifier: &str) -> bool { + self.surface.has_incomplete_module_qualifier(db, qualifier) + } + + fn candidate_names( + &self, + db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + ) -> Vec { + self.surface.candidate_names(db, namespace) + } + + fn private_candidate( + &self, + db: &'db dyn hir::Db, + namespace: hir_nameres::Namespace, + qualifier: &str, + name: &str, + ) -> Option { + self.surface + .private_candidate(db, namespace, qualifier, name) + } +} + +/// Summary returned by full resolution queries. +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] +pub struct FullResolutionSummary { + /// `true` once full resolution has traversed the module. + pub checked: bool, +} diff --git a/crates/nameres/src/modes.rs b/crates/nameres/src/modes.rs new file mode 100644 index 00000000..283df177 --- /dev/null +++ b/crates/nameres/src/modes.rs @@ -0,0 +1,70 @@ +use super::hir_nameres; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(super) enum ExportResolutionMode { + Lenient, + Strict, +} + +impl ExportResolutionMode { + pub(super) fn is_strict(self) -> bool { + matches!(self, Self::Strict) + } + + pub(super) fn suppress_if(self, suppress: bool) -> Self { + match (self, suppress) { + (Self::Strict, false) => Self::Strict, + _ => Self::Lenient, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(super) enum CtorInclusion { + Exclude, + #[allow(dead_code)] + Include, +} + +impl CtorInclusion { + pub(super) fn includes_data_ctors(self) -> bool { + matches!(self, Self::Include) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(super) enum BodyDiagnosticPolicy { + Emit, + SuppressForParseErrors, +} + +impl BodyDiagnosticPolicy { + pub(super) fn from_parse_errors(has_parse_errors: bool) -> Self { + if has_parse_errors { + Self::SuppressForParseErrors + } else { + Self::Emit + } + } + + pub(super) fn from_suppress_for_parse_errors(suppress_for_parse_errors: bool) -> Self { + if suppress_for_parse_errors { + Self::SuppressForParseErrors + } else { + Self::Emit + } + } + + pub(super) fn as_hir_policy(self) -> hir_nameres::NameresDiagnosticPolicy { + match self { + Self::Emit => hir_nameres::NameresDiagnosticPolicy::Emit, + Self::SuppressForParseErrors => { + hir_nameres::NameresDiagnosticPolicy::SuppressForParseErrors + } + } + } + + pub(super) fn suppress_for_parse_errors(self) -> bool { + matches!(self, Self::SuppressForParseErrors) + } +} diff --git a/crates/nameres/src/paths.rs b/crates/nameres/src/paths.rs new file mode 100644 index 00000000..53d5caee --- /dev/null +++ b/crates/nameres/src/paths.rs @@ -0,0 +1,210 @@ +use super::*; + +/// Resolves a module path reference to a logical module and candidate file +/// path. +/// +/// This function does not require the target module to already be loaded. The +/// driver uses it to discover reachable files before the tracked +/// [`resolve_module_path`] query enforces presence in the database. +pub fn resolve_module_path_candidate<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + path: &ModulePathRef<'db>, +) -> Result, Box>> { + let segments = path_segments(db, path); + let tree = db.module_tree(); + + let (library, logical_path, root) = if path.external.is_some() { + let Some((lib_name, rest)) = segments.split_first() else { + return Err(Box::new(module_not_found_diag(db, path, None))); + }; + let Some(root) = tree.external_roots(db).get(lib_name).cloned() else { + return Err(Box::new(missing_external_root_diag(db, path, lib_name))); + }; + let logical_path = if rest.is_empty() { + vec![lib_name.clone()] + } else { + rest.to_vec() + }; + (LibraryId::External(lib_name.clone()), logical_path, root) + } else if segments.first().is_some_and(|segment| segment == "std") { + let logical_path = if segments.len() == 1 { + vec!["std".to_owned()] + } else { + segments[1..].to_vec() + }; + let std_root = tree.std_root(db).clone(); + if segments.len() > 1 && !module_file_exists(db, &std_root, &logical_path) { + let library = importing.library(db).clone(); + let root = root_for_library(db, tree, &library, path)?; + let mut local_path = module_directory(importing.logical_path(db)); + local_path.extend(segments.clone()); + if module_file_exists(db, &root, &local_path) { + (library, local_path, root) + } else { + (LibraryId::Std, logical_path, std_root) + } + } else { + (LibraryId::Std, logical_path, std_root) + } + } else if segments.first().is_some_and(|segment| segment == "lib") && segments.len() > 1 { + let library = importing.library(db).clone(); + let root = root_for_library(db, tree, &library, path)?; + let mut logical_path = multi_root_workspace_prefix(db, importing); + logical_path.extend_from_slice(&segments[1..]); + (library, logical_path, root) + } else { + let library = importing.library(db).clone(); + let root = root_for_library(db, tree, &library, path)?; + let mut logical_path = module_directory(importing.logical_path(db)); + logical_path.extend(segments); + (library, logical_path, root) + }; + + let module = ModuleId::new(db, library, logical_path.clone()); + let file_path = root.join(module_file_path(&logical_path)); + Ok(ResolvedModulePath { module, file_path }) +} + +/// Preserves the LSP's internal root namespace for `lib.*` imports. +/// +/// Normally `lib.foo` is absolute below `/main`. Multi-root editor sessions +/// place each client workspace folder below a reserved two-segment prefix, so +/// the effective absolute root is that folder's namespace instead. Open files +/// detached from a removed folder retain the same isolation until they close. +fn multi_root_workspace_prefix(db: &dyn Db, importing: ModuleId<'_>) -> Vec { + if importing.library(db) != &LibraryId::Main { + return Vec::new(); + } + main_workspace_prefix(importing.logical_path(db)).to_vec() +} + +/// Resolves a module path reference to a loaded module. +/// +/// Returns a diagnostic when the path cannot be mapped to a library root or +/// when the target source file has not been loaded into the database. +#[salsa::tracked] +#[tracing::instrument( + target = "nameres::query", + level = "debug", + skip(db, importing, path), + fields(module = field::Empty) +)] +pub fn resolve_module_path<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + path: ModulePathRef<'db>, +) -> Result, Box>> { + record_module_field(db, importing); + let resolved = match resolve_module_path_candidate(db, importing, &path) { + Ok(resolved) => resolved, + Err(diagnostic) => { + trace_import_decision(db, importing, &path, None, "candidate-error"); + return Err(diagnostic); + } + }; + if db.module_file(resolved.module).is_some() { + trace_import_decision(db, importing, &path, Some(resolved.module), "loaded"); + Ok(resolved.module) + } else { + trace_import_decision(db, importing, &path, Some(resolved.module), "not-loaded"); + let suggestion = module_path_suggestion(db, &path, &resolved.file_path); + Err(Box::new(module_not_found_diag(db, &path, suggestion))) + } +} + +/// Resolves the module named directly by an import declaration. +/// +/// This applies the same library-root, standard-library fallback, and loaded +/// source checks as [`resolve_module_path`]. It does not interpret the import's +/// selector, alias, or hiding list. +pub fn resolve_direct_import_target<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + import: Import<'db>, +) -> Result, Box>> { + let path = path_ref_from_import(db, import); + resolve_module_path(db, importing, path) +} + +/// Resolves the candidate module named directly by an import declaration. +/// +/// Unlike [`resolve_direct_import_target`], this does not require the target +/// source to have been loaded into the database. The returned logical module +/// identity still applies standard-library fallback rules using the immutable +/// filesystem snapshot. +pub fn resolve_direct_import_target_candidate<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + import: Import<'db>, +) -> Result, Box>> { + let path = path_ref_from_import(db, import); + resolve_module_path_candidate(db, importing, &path) +} + +fn root_for_library<'db>( + db: &'db dyn Db, + tree: ModuleTree, + library: &LibraryId, + path: &ModulePathRef<'db>, +) -> Result>> { + match library { + LibraryId::Main => Ok(tree.main_root(db).clone()), + LibraryId::Std => Ok(tree.std_root(db).clone()), + LibraryId::External(name) => tree + .external_roots(db) + .get(name) + .cloned() + .ok_or_else(|| Box::new(missing_external_root_diag(db, path, name))), + } +} + +fn module_directory(path: &[String]) -> Vec { + path.split_last() + .map(|(_, prefix)| prefix.to_vec()) + .unwrap_or_default() +} + +fn module_file_exists(db: &dyn Db, root: &Path, logical_path: &[String]) -> bool { + let file_path = root.join(module_file_path(logical_path)); + db.module_fs_snapshot() + .existing_files(db) + .contains(&file_path) +} + +pub(super) fn path_segments<'db>(db: &'db dyn Db, path: &ModulePathRef<'db>) -> Vec { + path.segments + .iter() + .map(|segment| ident_text(db, *segment.atom())) + .collect() +} + +pub(super) fn module_path_span<'db>(db: &'db dyn Db, path: &ModulePathRef<'db>) -> Span<'db> { + let Some(first) = path.segments.first() else { + return path.span; + }; + let last = path.segments.last().expect("non-empty module path"); + first.span(db) + last.span(db) +} + +fn module_path_suggestion<'db>( + db: &'db dyn Db, + path: &ModulePathRef<'db>, + file_path: &Path, +) -> Option { + let parent = file_path.parent()?; + let requested = file_path.file_stem()?.to_str()?; + let mut segments = path_segments(db, path); + let candidates = db + .module_fs_snapshot() + .sibling_stems(db) + .get(parent)? + .clone(); + let suggestion = best_name_suggestion(requested, candidates)?; + if let Some(last) = segments.last_mut() { + *last = suggestion; + Some(segments.join(".")) + } else { + Some(suggestion) + } +} diff --git a/crates/nameres/src/scc.rs b/crates/nameres/src/scc.rs new file mode 100644 index 00000000..1dcbc460 --- /dev/null +++ b/crates/nameres/src/scc.rs @@ -0,0 +1,79 @@ +use super::*; + +/// Computes strongly connected components of a module graph. +/// +/// Components are based on reference edges, not only imports, so export cycles +/// are represented in the same graph used by interface fixed points. +pub fn strongly_connected_components<'db>(graph: &ModuleGraph<'db>) -> Vec>> { + let mut adjacency: FxHashMap, Vec>> = FxHashMap::default(); + for module in &graph.modules { + adjacency.entry(*module).or_default(); + } + for edge in &graph.reference_edges { + adjacency.entry(edge.from).or_default().push(edge.to); + } + + let mut state = TarjanState { + next_index: 0, + stack: Vec::new(), + on_stack: FxHashSet::default(), + indices: FxHashMap::default(), + lowlinks: FxHashMap::default(), + components: Vec::new(), + }; + + for module in &graph.modules { + if !state.indices.contains_key(module) { + strong_connect(*module, &adjacency, &mut state); + } + } + + state.components +} + +struct TarjanState<'db> { + next_index: usize, + stack: Vec>, + on_stack: FxHashSet>, + indices: FxHashMap, usize>, + lowlinks: FxHashMap, usize>, + components: Vec>>, +} + +fn strong_connect<'db>( + module: ModuleId<'db>, + adjacency: &FxHashMap, Vec>>, + state: &mut TarjanState<'db>, +) { + let index = state.next_index; + state.next_index += 1; + state.indices.insert(module, index); + state.lowlinks.insert(module, index); + state.stack.push(module); + state.on_stack.insert(module); + + for target in adjacency.get(&module).into_iter().flatten() { + if !state.indices.contains_key(target) { + strong_connect(*target, adjacency, state); + let target_low = state.lowlinks[target]; + let module_low = state.lowlinks.get_mut(&module).expect("module lowlink"); + *module_low = (*module_low).min(target_low); + } else if state.on_stack.contains(target) { + let target_index = state.indices[target]; + let module_low = state.lowlinks.get_mut(&module).expect("module lowlink"); + *module_low = (*module_low).min(target_index); + } + } + + if state.lowlinks[&module] == state.indices[&module] { + let mut component = Vec::new(); + while let Some(popped) = state.stack.pop() { + state.on_stack.remove(&popped); + component.push(popped); + if popped == module { + break; + } + } + state.components.push(component); + } +} diff --git a/crates/nameres/src/util.rs b/crates/nameres/src/util.rs new file mode 100644 index 00000000..4cf341b2 --- /dev/null +++ b/crates/nameres/src/util.rs @@ -0,0 +1,537 @@ +use std::fmt; + +use super::*; + +/// Borrowed display adapter for logical module IDs. +#[derive(Clone, Copy)] +pub struct ModuleDisplay<'db> { + db: &'db dyn Db, + module: ModuleId<'db>, +} + +impl<'db> ModuleDisplay<'db> { + /// Creates a display adapter for `module`. + pub fn new(db: &'db dyn Db, module: ModuleId<'db>) -> Self { + Self { db, module } + } +} + +impl fmt::Display for ModuleDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let path = self.module.logical_path(self.db); + match self.module.library(self.db) { + LibraryId::Main => write_dot_segments(f, path.iter().map(String::as_str)), + LibraryId::Std if path.as_slice() == ["std"] => f.write_str("std"), + LibraryId::Std => { + f.write_str("std.")?; + write_dot_segments(f, path.iter().map(String::as_str)) + } + LibraryId::External(name) => { + write!(f, "@{name}.")?; + write_dot_segments(f, path.iter().map(String::as_str)) + } + } + } +} + +impl fmt::Debug for ModuleDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl PartialEq<&str> for ModuleDisplay<'_> { + fn eq(&self, other: &&str) -> bool { + let path = self.module.logical_path(self.db); + match self.module.library(self.db) { + LibraryId::Main => dot_segments_eq(path.iter().map(String::as_str), other), + LibraryId::Std if path.as_slice() == ["std"] => *other == "std", + LibraryId::Std => other + .strip_prefix("std.") + .is_some_and(|tail| dot_segments_eq(path.iter().map(String::as_str), tail)), + LibraryId::External(name) => other + .strip_prefix('@') + .and_then(|tail| tail.strip_prefix(name.as_str())) + .and_then(|tail| tail.strip_prefix('.')) + .is_some_and(|tail| dot_segments_eq(path.iter().map(String::as_str), tail)), + } + } +} + +impl PartialEq for ModuleDisplay<'_> { + fn eq(&self, other: &String) -> bool { + PartialEq::<&str>::eq(self, &other.as_str()) + } +} + +/// Borrowed display adapter for module paths as written in import/export +/// syntax. +#[derive(Clone, Copy)] +pub struct ModulePathDisplay<'a, 'db> { + db: &'db dyn Db, + path: &'a ModulePathRef<'db>, +} + +impl<'a, 'db> ModulePathDisplay<'a, 'db> { + /// Creates a display adapter for `path`. + pub fn new(db: &'db dyn Db, path: &'a ModulePathRef<'db>) -> Self { + Self { db, path } + } +} + +impl fmt::Display for ModulePathDisplay<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.path.external.is_some() { + f.write_str("@")?; + } + write_dot_segments(f, module_path_segment_texts(self.db, self.path)) + } +} + +impl fmt::Debug for ModulePathDisplay<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl PartialEq<&str> for ModulePathDisplay<'_, '_> { + fn eq(&self, other: &&str) -> bool { + if self.path.external.is_some() { + other.strip_prefix('@').is_some_and(|tail| { + dot_segments_eq(module_path_segment_texts(self.db, self.path), tail) + }) + } else { + dot_segments_eq(module_path_segment_texts(self.db, self.path), other) + } + } +} + +impl PartialEq for ModulePathDisplay<'_, '_> { + fn eq(&self, other: &String) -> bool { + PartialEq::<&str>::eq(self, &other.as_str()) + } +} + +fn write_dot_segments<'a>( + f: &mut fmt::Formatter<'_>, + segments: impl IntoIterator, +) -> fmt::Result { + let mut first = true; + for segment in segments { + if first { + first = false; + } else { + f.write_str(".")?; + } + f.write_str(segment)?; + } + Ok(()) +} + +fn dot_segments_eq<'a>(segments: impl IntoIterator, text: &str) -> bool { + let mut tail = text; + let mut first = true; + for segment in segments { + if first { + first = false; + } else if let Some(next) = tail.strip_prefix('.') { + tail = next; + } else { + return false; + } + let Some(next) = tail.strip_prefix(segment) else { + return false; + }; + tail = next; + } + tail.is_empty() +} + +fn module_path_segment_texts<'a, 'db>( + db: &'db dyn Db, + path: &'a ModulePathRef<'db>, +) -> impl Iterator + 'a +where + 'db: 'a, +{ + path.segments + .iter() + .map(move |segment| (*segment.atom()).text(db)) +} + +/// Formats a logical module ID as user-facing text. +/// +/// Main modules omit a prefix, standard-library modules use `std`, and external +/// modules use `@name.path` form. +pub fn module_id_display<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> String { + module.display(db).to_string() +} + +/// Formats a module path reference as it appeared in import/export syntax. +pub fn module_path_display<'db>(db: &'db dyn Db, path: &ModulePathRef<'db>) -> String { + ModulePathDisplay::new(db, path).to_string() +} + +/// Returns the internal two-segment LSP workspace scope prefix, if present. +/// +/// This deliberately recognizes only the namespace shape emitted by the LSP; +/// ordinary source directories with similar names remain normal module paths. +pub(super) fn main_workspace_prefix(logical_path: &[String]) -> &[String] { + match logical_path { + [prefix, namespace, ..] + if matches!( + prefix.as_str(), + "__solcore_workspace__" | "__solcore_detached__" + ) && namespace.len() >= 16 + && namespace.bytes().all(|byte| byte.is_ascii_hexdigit()) => + { + &logical_path[..2] + } + _ => &logical_path[..0], + } +} + +/// Converts a logical module path into the conventional source file path. +/// +/// Each logical segment becomes a path component and the file extension is +/// `.solc`. +pub fn module_file_path(logical_path: &[String]) -> PathBuf { + let mut path = PathBuf::new(); + for segment in logical_path { + path.push(segment); + } + path.set_extension("solc"); + path +} + +/// Converts an absolute file path under `root` into a logical module key. +/// +/// Returns `None` when `file_path` is outside `root`, contains non-UTF-8 path +/// segments, or maps to an empty logical path. +pub fn module_key_for_path(library: LibraryId, root: &Path, file_path: &Path) -> Option { + let rel = file_path.strip_prefix(root).ok()?; + let mut logical_path = Vec::new(); + for component in rel.with_extension("").components() { + match component { + std::path::Component::Normal(segment) => { + logical_path.push(segment.to_str()?.to_owned()); + } + std::path::Component::CurDir => {} + std::path::Component::ParentDir + | std::path::Component::RootDir + | std::path::Component::Prefix(_) => return None, + } + } + (!logical_path.is_empty()).then_some(ModuleKey { + library, + logical_path, + }) +} + +/// Interns a logical module key in the current database. +pub fn module_id_from_key<'db>(db: &'db dyn Db, key: &ModuleKey) -> ModuleId<'db> { + ModuleId::new(db, key.library.clone(), key.logical_path.clone()) +} + +/// Finds the logical module identity corresponding to a source file. +/// +/// When roots overlap, an identity whose loaded source is exactly `file` wins. +/// Otherwise this returns the first identity derivable from the configured +/// roots, which also supports compiler-owned HIR overlays that retain the +/// original file URL. Test and in-memory drivers may use the canonical virtual +/// URL shape `memory:///main/...`, `memory:///std/...`, or +/// `memory:///external//...`; those identities are accepted only when +/// the resulting module is loaded as exactly `file`. +pub fn module_id_for_source_file<'db>(db: &'db dyn Db, file: SourceFile) -> Option> { + let tree = db.module_tree(); + let mut candidates = Vec::new(); + if let Some(path) = hir::url_to_file_path(file.url(db)) { + if let Some(key) = module_key_for_path(LibraryId::Main, tree.main_root(db), &path) { + candidates.push(module_id_from_key(db, &key)); + } + if let Some(key) = module_key_for_path(LibraryId::Std, tree.std_root(db), &path) { + candidates.push(module_id_from_key(db, &key)); + } + for (name, root) in tree.external_roots(db) { + if let Some(key) = module_key_for_path(LibraryId::External(name.clone()), root, &path) { + candidates.push(module_id_from_key(db, &key)); + } + } + } + let rooted = candidates + .iter() + .copied() + .find(|candidate| db.module_file(*candidate) == Some(file)) + .or_else(|| candidates.into_iter().next()); + rooted.or_else(|| virtual_module_id_for_source_file(db, file)) +} + +fn virtual_module_id_for_source_file<'db>( + db: &'db dyn Db, + file: SourceFile, +) -> Option> { + let url = file.url(db); + if url.scheme() != "memory" { + return None; + } + let segments = url + .path_segments()? + .filter(|segment| !segment.is_empty()) + .map(str::to_owned) + .collect::>(); + let (library, mut logical_path) = match segments.as_slice() { + [root, rest @ ..] if root == "main" => (LibraryId::Main, rest.to_vec()), + [root, rest @ ..] if root == "std" => (LibraryId::Std, rest.to_vec()), + [root, name, rest @ ..] if root == "external" => { + (LibraryId::External(name.clone()), rest.to_vec()) + } + _ => return None, + }; + let last = logical_path.last_mut()?; + *last = last.strip_suffix(".solc")?.to_owned(); + if last.is_empty() { + return None; + } + let candidate = module_id_from_key( + db, + &ModuleKey { + library, + logical_path, + }, + ); + (db.module_file(candidate) == Some(file)).then_some(candidate) +} + +pub(super) fn record_source_file_field(db: &dyn Db, file: SourceFile) { + if tracing::enabled!(target: "nameres::query", Level::DEBUG) { + tracing::Span::current().record("file", field::display(file_url_tail(db, file))); + } +} + +pub(super) fn record_module_field<'db>(db: &'db dyn Db, module: ModuleId<'db>) { + if tracing::enabled!(target: "nameres::query", Level::DEBUG) { + let span = tracing::Span::current(); + span.record("module", field::display(module.display(db))); + if let Some(file) = db.module_file(module) { + span.record("file", field::display(file_url_tail(db, file))); + } + } +} + +pub(super) fn record_body_field<'db>(db: &'db dyn Db, body: FuncBody<'db>) { + if tracing::enabled!(target: "nameres::query", Level::DEBUG) { + let def = body.def_id(db); + let span = tracing::Span::current(); + span.record("file", field::display(file_url_tail(db, def.file(db)))); + span.record("def", field::display(def_name(db, def))); + } +} + +fn def_name<'db>(db: &'db dyn Db, def: DefId<'db>) -> String { + def.name(db) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| format!("{:?}", def.kind(db))) +} + +fn file_url_tail(db: &dyn hir::Db, file: SourceFile) -> String { + let url = file.url(db); + if let Some(mut segments) = url.path_segments() + && let Some(last) = segments.next_back() + && !last.is_empty() + { + return last.to_owned(); + } + url.as_str() + .rsplit('/') + .next() + .filter(|tail| !tail.is_empty()) + .unwrap_or(url.as_str()) + .to_owned() +} + +pub(super) fn trace_import_decision<'db>( + db: &'db dyn Db, + importing: ModuleId<'db>, + path: &ModulePathRef<'db>, + target: Option>, + status: &'static str, +) { + if tracing::enabled!(target: "nameres::imports", Level::TRACE) { + match target { + Some(target) => { + tracing::trace!( + target: "nameres::imports", + module = %importing.display(db), + path = %ModulePathDisplay::new(db, path), + target = %target.display(db), + status, + "import resolution decision" + ); + } + None => { + tracing::trace!( + target: "nameres::imports", + module = %importing.display(db), + path = %ModulePathDisplay::new(db, path), + target = "", + status, + "import resolution decision" + ); + } + } + } +} + +pub(super) fn selector_kind<'db>(selector: &ImportSelector<'db>) -> &'static str { + match selector { + ImportSelector::Wildcard => "wildcard", + ImportSelector::Names(_) => "names", + } +} + +pub(super) fn ident_text<'db>(db: &'db dyn Db, ident: Ident<'db>) -> String { + ident.name(db).clone() +} + +pub(super) fn spanned_name_text<'db>( + db: &'db dyn Db, + name: &SpannedElem<'db, Ident<'db>>, +) -> String { + ident_text(db, *name.atom()) +} + +pub(super) fn unique_strings(values: impl IntoIterator) -> Vec { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value.clone()) { + result.push(value); + } + } + result +} + +pub(super) fn best_name_suggestion( + name: &str, + candidates: impl IntoIterator, +) -> Option { + let mut candidates = candidates + .into_iter() + .filter(|candidate| candidate != name) + .collect::>(); + candidates.sort(); + candidates.dedup(); + + let mut best: Option<(usize, String)> = None; + for candidate in candidates { + let distance = edit_distance(name, &candidate); + let limit = suggestion_distance_limit(name, &candidate); + if distance == 0 || distance > limit { + continue; + } + match &best { + Some((best_distance, best_candidate)) + if distance > *best_distance + || (distance == *best_distance && candidate >= *best_candidate) => {} + _ => best = Some((distance, candidate)), + } + } + best.map(|(_, candidate)| candidate) +} + +fn suggestion_distance_limit(left: &str, right: &str) -> usize { + let max_len = left.chars().count().max(right.chars().count()); + if max_len <= 4 { 1 } else { 3 } +} + +fn edit_distance(left: &str, right: &str) -> usize { + let right_chars = right.chars().collect::>(); + let mut previous = (0..=right_chars.len()).collect::>(); + let mut current = vec![0; right_chars.len() + 1]; + + for (left_index, left_char) in left.chars().enumerate() { + current[0] = left_index + 1; + for (right_index, right_char) in right_chars.iter().enumerate() { + let substitution = usize::from(left_char != *right_char); + current[right_index + 1] = (previous[right_index + 1] + 1) + .min(current[right_index] + 1) + .min(previous[right_index] + substitution); + } + previous.clone_from(¤t); + } + + previous[right_chars.len()] +} + +pub(super) fn unique_modules<'db>( + values: impl IntoIterator>, +) -> Vec> { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value) { + result.push(value); + } + } + result +} + +pub(super) fn unique_origins<'db>( + values: impl IntoIterator>, +) -> Vec> { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value.clone()) { + result.push(value); + } + } + result +} + +pub(super) fn sorted_namespaces(values: impl IntoIterator) -> Vec { + let mut seen = FxHashSet::default(); + let mut result = Vec::new(); + for value in values { + if seen.insert(value) { + result.push(value); + } + } + result.sort_by_key(|namespace| namespace_sort_key(*namespace)); + result +} + +fn namespace_name(namespace: Namespace) -> &'static str { + match namespace { + Namespace::Term => "term", + Namespace::Type => "type", + Namespace::Class => "trait", + } +} + +pub(super) fn namespace_context(namespaces: &[Namespace]) -> String { + let names = namespaces + .iter() + .map(|namespace| namespace_name(*namespace)) + .collect::>() + .join("/"); + if namespaces.len() == 1 { + format!("in {names} namespace") + } else { + format!("across {names} namespaces") + } +} + +pub(super) fn private_surface_key( + namespace: hir_nameres::Namespace, + qualifier: &str, + name: &str, +) -> String { + let prefix = match namespace { + hir_nameres::Namespace::Term => "term", + hir_nameres::Namespace::Type => "type", + hir_nameres::Namespace::Field => "field", + hir_nameres::Namespace::Module => "module", + }; + format!("{prefix}:{qualifier}.{name}") +} diff --git a/crates/nameres/src/validation.rs b/crates/nameres/src/validation.rs new file mode 100644 index 00000000..de41ad7c --- /dev/null +++ b/crates/nameres/src/validation.rs @@ -0,0 +1,415 @@ +use super::*; + +/// Validates imports and exports for one loaded module. +/// +/// The public interface is forced before duplicate export validation so checks +/// that depend on re-exported interfaces see the converged value. +#[salsa::tracked] +pub fn validate_module<'db>(db: &'db dyn Db, module: ModuleId<'db>) -> ValidationSummary { + let _ = public_interface(db, module); + ValidationSummary { checked: true } +} + +/// Validates every module reachable from `entry`. +#[salsa::tracked] +pub fn validate_reachable<'db>(db: &'db dyn Db, entry: ModuleId<'db>) -> Vec> { + let modules = reachable_modules(db, entry); + for module in &modules { + validate_module(db, *module); + } + modules +} + +pub(super) fn validate_imports<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + diagnostics: &mut Vec>, +) { + let Some(file) = db.module_file(module) else { + return; + }; + let module_items = module_imports(db, file); + validate_duplicate_qualifiers(db, &module_items.imports, diagnostics); + validate_duplicate_selectors(db, &module_items.imports, diagnostics); + validate_import_items_exist(db, module, &module_items.imports, diagnostics); + validate_ambiguous_selected_imports(db, module, &module_items.imports, diagnostics); +} + +fn validate_duplicate_qualifiers<'db>( + db: &'db dyn Db, + imports: &[Import<'db>], + diagnostics: &mut Vec>, +) { + let mut seen: FxHashMap> = FxHashMap::default(); + for import in imports { + let Some((name, span)) = import_qualifier(db, *import) else { + continue; + }; + if let Some(first_span) = seen.get(&name) { + diagnostics.push(duplicate_qualifier_diag(db, *first_span, span, &name)); + } else { + seen.insert(name, span); + } + } +} + +fn validate_duplicate_selectors<'db>( + db: &'db dyn Db, + imports: &[Import<'db>], + diagnostics: &mut Vec>, +) { + for import in imports { + let Some(selector) = import.selector(db) else { + continue; + }; + if let ImportSelector::Names(names) = selector { + validate_duplicate_selected_names(db, names, diagnostics); + } + validate_duplicate_hidden_names(db, import.hiding(db), diagnostics); + } +} + +fn validate_duplicate_selected_names<'db>( + db: &'db dyn Db, + names: &[SelectedName<'db>], + diagnostics: &mut Vec>, +) { + let mut sources: FxHashMap> = FxHashMap::default(); + let mut locals: FxHashMap> = FxHashMap::default(); + let mut emitted: FxHashSet<(String, Span<'db>, Span<'db>)> = FxHashSet::default(); + for selected in names { + let source = spanned_name_text(db, &selected.name); + if let Some(first_span) = sources.get(&source) { + emit_duplicate_selector_once( + db, + &mut emitted, + diagnostics, + *first_span, + selected.name.span(db), + &source, + ); + } else { + sources.insert(source.clone(), selected.name.span(db)); + } + let local = selected + .alias + .as_ref() + .map(|alias| (spanned_name_text(db, alias), alias.span(db))) + .unwrap_or_else(|| (source, selected.name.span(db))); + if let Some(first_span) = locals.get(&local.0) { + emit_duplicate_selector_once( + db, + &mut emitted, + diagnostics, + *first_span, + local.1, + &local.0, + ); + } else { + locals.insert(local.0, local.1); + } + } +} + +fn emit_duplicate_selector_once<'db>( + db: &'db dyn Db, + emitted: &mut FxHashSet<(String, Span<'db>, Span<'db>)>, + diagnostics: &mut Vec>, + first: Span<'db>, + second: Span<'db>, + name: &str, +) { + if emitted.insert((name.to_owned(), first, second)) { + diagnostics.push(duplicate_selector_diag(db, first, second, name)); + } +} + +fn validate_duplicate_hidden_names<'db>( + db: &'db dyn Db, + names: &[ImportHiddenName<'db>], + diagnostics: &mut Vec>, +) { + let mut seen: FxHashMap> = FxHashMap::default(); + for hidden in names { + let name = spanned_name_text(db, &hidden.name); + if let Some(first_span) = seen.get(&name) { + diagnostics.push(duplicate_selector_diag( + db, + *first_span, + hidden.name.span(db), + &name, + )); + } else { + seen.insert(name, hidden.name.span(db)); + } + } +} + +fn validate_import_items_exist<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + imports: &[Import<'db>], + diagnostics: &mut Vec>, +) { + for import in imports { + let Some(selector) = import.selector(db) else { + continue; + }; + let path = path_ref_from_import(db, *import); + let Some(target) = resolve_for_export( + db, + module, + &path, + ExportResolutionMode::Lenient, + diagnostics, + ) else { + continue; + }; + if module_has_parse_errors(db, target) { + continue; + } + let interface = public_interface(db, target); + let available_names = interface_names(&interface); + if let ImportSelector::Names(names) = selector { + for selected in names { + let name = spanned_name_text(db, &selected.name); + if !available_names.contains(&name) { + tracing::trace!( + target: "nameres::imports", + module = %module.display(db), + target = %target.display(db), + name = %name, + "unknown selected import item" + ); + diagnostics.push(unknown_import_item_diag( + db, + selected.name.span(db), + &name, + Some(target), + best_name_suggestion(&name, available_names.iter().cloned()), + )); + } + } + } + for hidden in import.hiding(db) { + let name = spanned_name_text(db, &hidden.name); + if !available_names.contains(&name) { + tracing::trace!( + target: "nameres::imports", + module = %module.display(db), + target = %target.display(db), + name = %name, + "unknown hidden import item" + ); + diagnostics.push(unknown_import_item_diag( + db, + hidden.name.span(db), + &name, + Some(target), + best_name_suggestion(&name, available_names.iter().cloned()), + )); + } + } + } +} + +fn validate_ambiguous_selected_imports<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + imports: &[Import<'db>], + diagnostics: &mut Vec>, +) { + struct SelectedOccurrence<'db> { + namespace: Namespace, + target: ModuleId<'db>, + span: Span<'db>, + } + + let mut imported: FxHashMap>> = FxHashMap::default(); + for import in imports { + let Some(selector) = import.selector(db) else { + continue; + }; + let path = path_ref_from_import(db, *import); + let Some(target) = resolve_for_export( + db, + module, + &path, + ExportResolutionMode::Lenient, + diagnostics, + ) else { + continue; + }; + let interface = public_interface(db, target); + for item_ref in select_import_refs(db, &interface.item_refs, selector, import.hiding(db)) { + imported + .entry(item_ref.public_name) + .or_default() + .push(SelectedOccurrence { + namespace: item_ref.namespace, + target, + span: import.span(db), + }); + } + } + + let mut imported = imported.into_iter().collect::>(); + imported.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + + for (name, occurrences) in imported { + let all_targets = unique_modules(occurrences.iter().map(|occurrence| occurrence.target)); + if all_targets.len() <= 1 { + continue; + } + + let mut by_namespace: FxHashMap>> = + FxHashMap::default(); + for occurrence in &occurrences { + by_namespace + .entry(occurrence.namespace) + .or_default() + .push(occurrence); + } + let mut namespace_groups = by_namespace.into_iter().collect::>(); + namespace_groups.sort_by_key(|(namespace, _)| namespace_sort_key(*namespace)); + + let mut emitted_namespace_specific = false; + for (namespace, occurrences) in namespace_groups { + let targets = unique_modules(occurrences.iter().map(|occurrence| occurrence.target)); + if targets.len() > 1 { + let span = occurrences + .first() + .map(|occurrence| occurrence.span) + .or_else(|| module_root_span(db, module)); + diagnostics.push(ambiguous_import_diag( + db, + span, + &[namespace], + &name, + targets, + )); + emitted_namespace_specific = true; + } + } + + if !emitted_namespace_specific { + let namespaces = + sorted_namespaces(occurrences.iter().map(|occurrence| occurrence.namespace)); + let span = occurrences + .first() + .map(|occurrence| occurrence.span) + .or_else(|| module_root_span(db, module)); + diagnostics.push(ambiguous_import_diag( + db, + span, + &namespaces, + &name, + all_targets, + )); + } + } +} + +pub(super) fn validate_duplicate_exports<'db>( + db: &'db dyn Db, + module: ModuleId<'db>, + raw: &RawInterface<'db>, + diagnostics: &mut Vec>, +) { + let mut items: FxHashMap>> = FxHashMap::default(); + for item_ref in &raw.item_refs { + items + .entry(item_ref.item_ref.public_name.clone()) + .or_default() + .push(item_ref); + } + let mut items = items.into_iter().collect::>(); + items.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + + for (name, refs) in items { + let mut unique = Vec::<(ModuleId<'db>, &str)>::new(); + let mut duplicate_span = None; + for raw_ref in &refs { + let item_ref = &raw_ref.item_ref; + let key = (item_ref.origin.module, item_ref.source_name.as_str()); + if !unique + .iter() + .any(|(origin, source_name)| *origin == key.0 && *source_name == key.1) + { + if !unique.is_empty() && duplicate_span.is_none() { + duplicate_span = raw_ref.export_span; + } + unique.push(key); + } + } + if unique.len() > 1 { + let span = duplicate_span + .or_else(|| refs.first().and_then(|raw_ref| raw_ref.export_span)) + .or_else(|| module_root_span(db, module)); + diagnostics.push(duplicate_export_item_diag(db, span, &name)); + } + } + + let mut modules: FxHashMap>> = FxHashMap::default(); + for alias in &raw.module_aliases { + modules + .entry(alias.alias.public_name.clone()) + .or_default() + .push(alias); + } + let mut modules = modules.into_iter().collect::>(); + modules.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + + for (name, aliases) in modules { + let mut targets = Vec::>::new(); + let mut duplicate_span = None; + for raw_alias in &aliases { + let target = raw_alias.alias.target; + if !targets.contains(&target) { + if !targets.is_empty() && duplicate_span.is_none() { + duplicate_span = raw_alias.export_span; + } + targets.push(target); + } + } + if targets.len() > 1 { + let span = duplicate_span + .or_else(|| aliases.first().and_then(|raw_alias| raw_alias.export_span)) + .or_else(|| module_root_span(db, module)); + diagnostics.push(duplicate_export_module_diag(db, span, &name)); + } + } +} + +fn import_qualifier<'db>(db: &'db dyn Db, import: Import<'db>) -> Option<(String, Span<'db>)> { + if import.selector(db).is_some() { + return None; + } + import + .alias(db) + .map(|alias| (spanned_name_text(db, &alias), alias.span(db))) + .or_else(|| { + import + .path(db) + .last() + .map(|segment| (spanned_name_text(db, segment), segment.span(db))) + }) +} + +pub(super) fn default_module_binding_name<'db>( + db: &'db dyn Db, + path: &ModulePathRef<'db>, +) -> String { + path.segments + .last() + .map(|segment| spanned_name_text(db, segment)) + .unwrap_or_else(|| module_path_display(db, path)) +} + +pub(super) fn interface_names<'db>(interface: &Interface<'db>) -> FxHashSet { + interface + .item_refs + .iter() + .map(|item_ref| item_ref.public_name.clone()) + .collect() +} diff --git a/crates/nameres/tests/fixtures/ok/alias/main.solc b/crates/nameres/tests/fixtures/ok/alias/main.solc new file mode 100644 index 00000000..a2a878d8 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/alias/main.solc @@ -0,0 +1,3 @@ +import * as U from util; + +export util as PublicUtil; diff --git a/crates/nameres/tests/fixtures/ok/alias/util.solc b/crates/nameres/tests/fixtures/ok/alias/util.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/alias/util.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/nameres/tests/fixtures/ok/cycle/a.solc b/crates/nameres/tests/fixtures/ok/cycle/a.solc new file mode 100644 index 00000000..f7f4ec4d --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/cycle/a.solc @@ -0,0 +1,5 @@ +export b.{fb}; + +function fa() {} + +export { fa }; diff --git a/crates/nameres/tests/fixtures/ok/cycle/b.solc b/crates/nameres/tests/fixtures/ok/cycle/b.solc new file mode 100644 index 00000000..1c617c93 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/cycle/b.solc @@ -0,0 +1,5 @@ +export a.{fa}; + +function fb() {} + +export { fb }; diff --git a/crates/nameres/tests/fixtures/ok/cycle/main.solc b/crates/nameres/tests/fixtures/ok/cycle/main.solc new file mode 100644 index 00000000..f88cb703 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/cycle/main.solc @@ -0,0 +1,3 @@ +import {fb} from a; + +function main() {} diff --git a/crates/nameres/tests/fixtures/ok/external/extroot/extmod.solc b/crates/nameres/tests/fixtures/ok/external/extroot/extmod.solc new file mode 100644 index 00000000..f41a2812 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/external/extroot/extmod.solc @@ -0,0 +1,3 @@ +function ext() {} + +export { ext }; diff --git a/crates/nameres/tests/fixtures/ok/external/main.solc b/crates/nameres/tests/fixtures/ok/external/main.solc new file mode 100644 index 00000000..24eb4225 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/external/main.solc @@ -0,0 +1,3 @@ +import {ext} from @pkg.extmod; + +function main() {} diff --git a/crates/nameres/tests/fixtures/ok/local_std_subpath/main.solc b/crates/nameres/tests/fixtures/ok/local_std_subpath/main.solc new file mode 100644 index 00000000..53699d87 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/local_std_subpath/main.solc @@ -0,0 +1,5 @@ +import {value} from std.a.b; + +function main(x: word) returns (word) { + return value(x); +} diff --git a/crates/nameres/tests/fixtures/ok/local_std_subpath/std/a/b.solc b/crates/nameres/tests/fixtures/ok/local_std_subpath/std/a/b.solc new file mode 100644 index 00000000..36dd500c --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/local_std_subpath/std/a/b.solc @@ -0,0 +1,5 @@ +function value(x: word) returns (word) { + return x; +} + +export { value }; diff --git a/crates/nameres/tests/fixtures/ok/plain/main.solc b/crates/nameres/tests/fixtures/ok/plain/main.solc new file mode 100644 index 00000000..d8afb553 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/plain/main.solc @@ -0,0 +1,3 @@ +import {value} from util; + +function main() {} diff --git a/crates/nameres/tests/fixtures/ok/plain/util.solc b/crates/nameres/tests/fixtures/ok/plain/util.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/plain/util.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/nameres/tests/fixtures/ok/reexport_chain/a.solc b/crates/nameres/tests/fixtures/ok/reexport_chain/a.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/reexport_chain/a.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/nameres/tests/fixtures/ok/reexport_chain/b.solc b/crates/nameres/tests/fixtures/ok/reexport_chain/b.solc new file mode 100644 index 00000000..741a13e5 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/reexport_chain/b.solc @@ -0,0 +1 @@ +export a.{value}; diff --git a/crates/nameres/tests/fixtures/ok/reexport_chain/main.solc b/crates/nameres/tests/fixtures/ok/reexport_chain/main.solc new file mode 100644 index 00000000..253d0f2f --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/reexport_chain/main.solc @@ -0,0 +1,3 @@ +import {value} from b; + +function main() {} diff --git a/crates/nameres/tests/fixtures/ok/selective_hiding/main.solc b/crates/nameres/tests/fixtures/ok/selective_hiding/main.solc new file mode 100644 index 00000000..1ee26e65 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/selective_hiding/main.solc @@ -0,0 +1,3 @@ +import {visible} from util; + +function main() {} diff --git a/crates/nameres/tests/fixtures/ok/selective_hiding/util.solc b/crates/nameres/tests/fixtures/ok/selective_hiding/util.solc new file mode 100644 index 00000000..35ff3ae9 --- /dev/null +++ b/crates/nameres/tests/fixtures/ok/selective_hiding/util.solc @@ -0,0 +1,5 @@ +function visible() {} + +function hidden() {} + +export { * }; diff --git a/crates/nameres/tests/incremental_cache.rs b/crates/nameres/tests/incremental_cache.rs new file mode 100644 index 00000000..adc24aa5 --- /dev/null +++ b/crates/nameres/tests/incremental_cache.rs @@ -0,0 +1,374 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use hir::{ast::item::Item, diag::DiagnosticId, input::SourceFile, nameres::BodyResolutionContext}; +use parser::parse_file_to_hir; +use salsa::Setter; +use solcore_nameres::{ + LibraryId, ModuleDiagnostic, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, + ModuleTree, body_diagnostics, module_diagnostics, module_env, module_id_from_key, +}; + +#[salsa::db] +#[derive(Clone)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: BTreeMap, + executed: Arc>>, +} + +impl Default for TestDb { + fn default() -> Self { + let executed = Arc::new(Mutex::new(Vec::new())); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + }))), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: BTreeMap::new(), + executed, + } + } +} + +impl TestDb { + fn take_executed(&self) -> Vec { + std::mem::take(&mut *self.executed.lock().expect("execution log lock")) + } + + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self.module_files.clone(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl solcore_nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.expect("test module tree initialized") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("test module filesystem snapshot initialized") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("test module file snapshot initialized") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[test] +fn module_diagnostics_backdates_after_same_module_body_literal_edit() { + let before = "function main() returns (word) {\n return 1;\n}\n"; + let after = "function main() returns (word) {\n return 2;\n}\n"; + let (mut db, file, key) = db_with_main(before); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "module_diagnostics"), + 1, + "{executed:#?}" + ); + assert_eq!( + query_executions(&executed, "body_diagnostics"), + 1, + "{executed:#?}" + ); + } + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + assert!(module_diagnostics(&db, module).is_empty()); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "body_diagnostics"), + 1, + "{executed:#?}" + ); + assert_eq!( + query_executions(&executed, "module_diagnostics"), + 0, + "{executed:#?}" + ); + } +} + +#[test] +fn body_diagnostics_key_excludes_module_env_diagnostics() { + let (db, file, key) = db_with_main("function main() returns (word) { return 1; }\n"); + let module = module_id_from_key(&db, &key); + let hir_module = parse_file_to_hir(&db, file).module(&db); + let body = hir_module + .items(&db) + .iter() + .find_map(|item| match *item { + Item::FunctionDef(function) => function.body(&db), + _ => None, + }) + .expect("main body"); + let context = BodyResolutionContext { + module: hir_module, + enclosing_contract: None, + params: Vec::new(), + type_vars: Vec::new(), + }; + let env = module_env(&db, module); + let mut diagnostic_only_variant = env.clone(); + diagnostic_only_variant + .diagnostics + .push(ModuleDiagnostic::DuplicateExportedItemName { + name: "diagnostic-only".to_owned(), + span: None, + }); + assert_ne!(env, diagnostic_only_variant); + assert_eq!( + env.import_surface(), + diagnostic_only_variant.import_surface() + ); + + let _ = db.take_executed(); + assert!(body_diagnostics(&db, body, context.clone(), env.import_surface(), false).is_empty()); + let executed = db.take_executed(); + assert_eq!(query_executions(&executed, "body_diagnostics"), 1); + + assert!( + body_diagnostics( + &db, + body, + context, + diagnostic_only_variant.import_surface(), + false, + ) + .is_empty() + ); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "body_diagnostics"), + 0, + "diagnostic-only ModuleEnv state must not re-key body resolution: {executed:#?}" + ); +} + +#[test] +fn duplicate_export_diagnostics_backdate_after_unrelated_body_length_edit() { + let before = + "export a.{f};\nexport b.{f};\n\nfunction unrelated() returns (word) {\n return 1;\n}\n"; + let after = "export a.{f};\nexport b.{f};\n\nfunction unrelated() returns (word) {\n return 123456789;\n}\n"; + let (mut db, file, key) = db_with_duplicate_export_main(before); + + let before_ids = { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let ids = diagnostic_ids_for_code(&db, module, "SC0111"); + assert_eq!(ids.len(), 1); + ids + }; + + file.set_content(&mut db).to(Some(after.to_owned())); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let after_ids = diagnostic_ids_for_code(&db, module, "SC0111"); + assert_eq!(after_ids, before_ids); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "module_diagnostics"), + 0, + "{executed:#?}" + ); + } +} + +#[test] +fn module_not_found_suggestion_tracks_fs_snapshot_edit() { + let (mut db, _file, key) = db_with_main("import * as utilx from utilx;\n"); + let snapshot = db + .module_fs_snapshot + .expect("test module filesystem snapshot initialized"); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let diagnostics = module_diagnostics(&db, module); + assert_eq!(diagnostics.len(), 1); + let lowered = diagnostics[0].lower(&db); + assert!( + !lowered + .helps + .iter() + .any(|help| help.contains("did you mean")) + ); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "resolve_module_path"), + 1, + "{executed:#?}" + ); + } + + let mut sibling_stems = snapshot.sibling_stems(&db).clone(); + sibling_stems.insert(PathBuf::from("/memory"), vec!["util".to_owned()]); + snapshot.set_sibling_stems(&mut db).to(sibling_stems); + + { + let module = module_id_from_key(&db, &key); + let _ = db.take_executed(); + let diagnostics = module_diagnostics(&db, module); + assert_eq!(diagnostics.len(), 1); + let lowered = diagnostics[0].lower(&db); + assert!( + lowered + .helps + .iter() + .any(|help| help == "did you mean `util`?"), + "{lowered:#?}" + ); + let executed = db.take_executed(); + assert_eq!( + query_executions(&executed, "resolve_module_path"), + 1, + "{executed:#?}" + ); + } +} + +fn db_with_main(content: &str) -> (TestDb, SourceFile, ModuleKey) { + let mut db = TestDb::default(); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory"), + PathBuf::from("/memory/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(empty_module_fs_snapshot(&db)); + let file = SourceFile::new( + &db, + "memory:///main.solc".parse().expect("valid URL"), + Some(content.to_owned()), + ); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + db.insert_module_file(key.clone(), file); + (db, file, key) +} + +fn db_with_duplicate_export_main(content: &str) -> (TestDb, SourceFile, ModuleKey) { + let mut db = TestDb::default(); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory"), + PathBuf::from("/memory/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(empty_module_fs_snapshot(&db)); + for (path, source) in [ + ( + vec!["a"], + "function f() returns (word) { return 0; }\nexport { f };\n", + ), + ( + vec!["b"], + "function f() returns (word) { return 0; }\nexport { f };\n", + ), + ] { + let key = ModuleKey { + library: LibraryId::Main, + logical_path: path.into_iter().map(str::to_owned).collect(), + }; + let file = source_file(&db, &key, source); + db.insert_module_file(key, file); + } + + let file = SourceFile::new( + &db, + "memory:///main.solc".parse().expect("valid URL"), + Some(content.to_owned()), + ); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + db.insert_module_file(key.clone(), file); + (db, file, key) +} + +fn empty_module_fs_snapshot(db: &TestDb) -> ModuleFsSnapshot { + ModuleFsSnapshot::new(db, BTreeSet::new(), BTreeMap::new()) +} + +fn source_file(db: &TestDb, key: &ModuleKey, content: &str) -> SourceFile { + let url = format!("memory:///{}.solc", key.logical_path.join("/")) + .parse() + .expect("valid URL"); + SourceFile::new(db, url, Some(content.to_owned())) +} + +fn diagnostic_ids_for_code(db: &TestDb, module: ModuleId<'_>, code: &str) -> Vec { + module_diagnostics(db, module) + .iter() + .filter(|diagnostic| diagnostic.lower(db).code.as_deref() == Some(code)) + .map(|diagnostic| diagnostic.diagnostic_id(db)) + .collect() +} + +fn query_executions(events: &[String], query: &str) -> usize { + events.iter().filter(|event| event.contains(query)).count() +} diff --git a/crates/nameres/tests/module_system.rs b/crates/nameres/tests/module_system.rs new file mode 100644 index 00000000..a3be9883 --- /dev/null +++ b/crates/nameres/tests/module_system.rs @@ -0,0 +1,2137 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Path, PathBuf}, +}; + +use annotate_snippets::Renderer; +use hir::{ + arena::Id, + ast::{ + function::{Expr, ExprKind, FuncBody}, + item::{ContractItem, Item, Module}, + }, + diag::{Diagnostic, sort_dedup_rendered_diagnostics}, + input::SourceFile, + nameres::{DefResolutionKind, Resolution, resolve_module_with_imports}, +}; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::Setter; +use solcore_nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleGraph, ModuleId, ModuleKey, ModuleTree, + Namespace, auto_import_candidates, auto_import_constructor_candidates, auto_import_index, + auto_import_module_candidates, module_diagnostics, module_env, module_id_from_key, + module_imports, module_key_for_path, public_interface, reachable_diagnostics, + resolve_module_path_candidate, resolve_reachable_full, source_import_path, + strongly_connected_components, +}; +use url::Url; + +#[salsa::db] +#[derive(Clone, Default)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, +} + +impl TestDb { + fn sync_module_files(&mut self) { + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + if snapshot.files(self) != &files { + snapshot.set_files(self).to(files); + } + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } + + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) != Some(file) { + self.sync_module_files(); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl solcore_nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.expect("test module tree initialized") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("test module filesystem snapshot initialized") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("test module file snapshot initialized") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[test] +fn module_keys_reject_parent_directory_components() { + let root = Path::new("workspace"); + let spelled_with_parent = Path::new("workspace/src/../src/main.solc"); + + assert!( + module_key_for_path(LibraryId::Main, root, spelled_with_parent).is_none(), + "unnormalized parent components must not enter logical module keys" + ); +} + +#[test] +fn plain_import_has_no_diagnostics() { + let fixture = fixture_dir("ok/plain"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (graph, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + assert_eq!(graph.modules.len(), 2); + + let util = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec!["util".to_owned()], + }, + ); + let interface = public_interface(&db, util); + assert!(interface.terms.contains_key("value")); +} + +#[test] +fn auto_imports_index_unreachable_public_symbols_and_rank_direct_exports_first() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "export { wanted }; function wanted() returns (word) { return 0; }", + ), + ( + vec!["direct"], + "export { wanted, Thing, Eqish }; function wanted() returns (word) { return 1; } enum Thing { Thing } trait Eqish {}", + ), + (vec!["wrapper"], "export direct.{wanted};"), + ( + vec!["private"], + "function wanted() returns (word) { return 2; }", + ), + ( + vec!["broken"], + "export { wanted }; lost(x: word) returns (word) { return 0; } function wanted() returns (word) { return 3; }", + ), + (vec!["broken_wrapper"], "export broken.{wanted};"), + ( + vec!["ambiguous"], + "export direct.{wanted}; export other.{wanted};", + ), + ( + vec!["other"], + "export { wanted }; function wanted() returns (word) { return 4; }", + ), + ( + vec!["term_collision"], + "export { Clash }; function Clash() returns (word) { return 5; }", + ), + ( + vec!["type_collision"], + "export { Clash }; enum Clash { Clash }", + ), + ( + vec!["namespace_ambiguous"], + "export term_collision.{Clash}; export type_collision.{Clash};", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let broken_wrapper = module_id_from_key(&db, &module_key(["broken_wrapper"])); + assert!( + public_interface(&db, broken_wrapper) + .terms + .contains_key("wanted") + ); + let namespace_ambiguous = module_id_from_key(&db, &module_key(["namespace_ambiguous"])); + let ambiguous_interface = public_interface(&db, namespace_ambiguous); + assert!(ambiguous_interface.terms.contains_key("Clash")); + assert!(ambiguous_interface.types.contains_key("Clash")); + + let candidates = auto_import_candidates(&db, importing, "wanted", Namespace::Term); + let paths = candidates + .iter() + .map(|candidate| candidate.import_path.as_str()) + .collect::>(); + assert_eq!(paths, ["lib.direct", "lib.other", "lib.wrapper"]); + assert!(!candidates[0].is_reexport()); + assert!(!candidates[1].is_reexport()); + assert!(candidates[2].is_reexport()); + assert!( + candidates + .iter() + .all(|candidate| candidate.public_name == "wanted") + ); + + let type_candidates = auto_import_candidates(&db, importing, "Thing", Namespace::Type); + assert_eq!(type_candidates.len(), 1); + assert_eq!(type_candidates[0].import_path, "lib.direct"); + assert!(auto_import_candidates(&db, importing, "Thing", Namespace::Term).is_empty()); + let class_candidates = auto_import_candidates(&db, importing, "Eqish", Namespace::Class); + assert_eq!(class_candidates.len(), 1); + assert_eq!(class_candidates[0].import_path, "lib.direct"); + assert_eq!( + auto_import_candidates(&db, importing, "Clash", Namespace::Term)[0].import_path, + "lib.term_collision" + ); + assert_eq!( + auto_import_candidates(&db, importing, "Clash", Namespace::Type)[0].import_path, + "lib.type_collision" + ); + + let index = auto_import_index(&db, importing); + assert!( + index + .iter() + .all(|candidate| candidate.provider != importing) + ); + assert!( + index + .iter() + .all(|candidate| candidate.import_path != "lib.private") + ); + assert!( + index + .iter() + .all(|candidate| candidate.import_path != "lib.broken") + ); + assert!( + index + .iter() + .all(|candidate| candidate.import_path != "lib.broken_wrapper") + ); + assert!( + index + .iter() + .all(|candidate| candidate.import_path != "lib.ambiguous") + ); + assert!( + index + .iter() + .all(|candidate| candidate.import_path != "lib.namespace_ambiguous") + ); +} + +#[test] +fn constructor_auto_imports_require_the_requested_constructor_to_be_visible() { + let (db, entry) = load_sources([ + (vec!["main"], "function main() {}"), + ( + vec!["full"], + "export { Option(*) }; enum Option { None, Some(word) }", + ), + ( + vec!["opaque"], + "export { Option }; enum Option { None, Some(word) }", + ), + ( + vec!["partial"], + "export { Option(Some) }; enum Option { None, Some(word) }", + ), + (vec!["wrapper"], "export full.{Option(Some)};"), + ]); + let importing = module_id_from_key(&db, &entry); + + let some_paths = auto_import_constructor_candidates(&db, importing, "Option", "Some") + .into_iter() + .map(|candidate| candidate.import_path) + .collect::>(); + assert_eq!(some_paths, ["lib.full", "lib.partial", "lib.wrapper"]); + + let none_paths = auto_import_constructor_candidates(&db, importing, "Option", "None") + .into_iter() + .map(|candidate| candidate.import_path) + .collect::>(); + assert_eq!(none_paths, ["lib.full"]); + assert!(auto_import_constructor_candidates(&db, importing, "Option", "Missing").is_empty()); + assert!(auto_import_constructor_candidates(&db, importing, "bad.name", "Some").is_empty()); +} + +#[test] +fn module_auto_imports_match_the_default_qualifier_and_public_member() { + let (db, entry) = load_sources([ + (vec!["main"], "function main() {}"), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ( + vec!["two", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + (vec!["aaa", "math"], "export lib.one.math.{value};"), + ( + vec!["private", "math"], + "function value() returns (word) { return 3; }", + ), + ( + vec!["broken", "math"], + "export { value }; lost(x: word) returns (word) { return 0; } function value() returns (word) { return 4; }", + ), + ( + vec!["other"], + "export { value }; function value() returns (word) { return 5; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + + let candidates = auto_import_module_candidates(&db, importing, "math", "value"); + let paths = candidates + .iter() + .map(|candidate| candidate.import_path.as_str()) + .collect::>(); + assert_eq!(paths, ["lib.one.math", "lib.two.math", "lib.aaa.math"]); + assert!( + candidates + .iter() + .all(|candidate| { candidate.qualifier == "math" && candidate.member == "value" }) + ); + assert!(auto_import_module_candidates(&db, importing, "other", "private").is_empty()); + assert!(auto_import_module_candidates(&db, importing, "one.math", "value").is_empty()); +} + +#[test] +fn module_auto_imports_require_an_immediate_term_member() { + let (db, entry) = load_sources([ + (vec!["main"], "function main() {}"), + ( + vec!["types", "math"], + "export { Value }; enum Value { Value(word) }", + ), + (vec!["aliases", "math"], "export lib.target as nested;"), + ( + vec!["target"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + + assert!(auto_import_module_candidates(&db, importing, "math", "Value").is_empty()); + assert!(auto_import_module_candidates(&db, importing, "math", "nested").is_empty()); +} + +#[test] +fn module_auto_imports_respect_explicit_namespace_aliases() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as math from lib.existing.math; function main() {}", + ), + ( + vec!["existing", "math"], + "export { old }; function old() returns (word) { return 1; }", + ), + ( + vec!["candidate", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); + + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as math from lib.existing; function main() {}", + ), + ( + vec!["existing"], + "export { old }; function old() returns (word) { return 1; }", + ), + ( + vec!["candidate", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); + + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as deep from lib.math.deep; function main() {}", + ), + ( + vec!["math", "deep"], + "export { old }; function old() returns (word) { return 1; }", + ), + ( + vec!["other", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let candidates = auto_import_module_candidates(&db, importing, "math", "value"); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.other.math"); +} + +#[test] +fn module_auto_imports_do_not_conflict_with_unqualified_bindings() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "function math() returns (word) { return 0; } function main() {}", + ), + ( + vec!["candidate", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); + + let (db, entry) = load_sources([ + ( + vec!["main"], + "import {math} from lib.names; function main() {}", + ), + ( + vec!["names"], + "export { math }; function math() returns (word) { return 0; }", + ), + ( + vec!["candidate", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); +} + +#[test] +fn module_qualifier_conflicts_with_selected_term_in_either_import_order() { + for imports in [ + "import * as util from util; import {util} from other;", + "import {util} from other; import * as util from util;", + ] { + let main = format!("{imports} function main() {{}}"); + let (db, entry) = load_sources([ + (vec!["main"], main.as_str()), + ( + vec!["util"], + "function value() returns (word) { return 0; }", + ), + ( + vec!["other"], + "export { util }; function util() returns (word) { return 1; }", + ), + ]); + let module = module_id_from_key(&db, &entry); + assert_eq!( + module_diagnostic_codes(&db, module), + ["SC0121"], + "import order should not affect qualifier conflicts: {imports}" + ); + } +} + +#[test] +fn module_auto_imports_check_every_generated_prefix_binding() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "function one() returns (word) { return 0; } function main() {}", + ), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ( + vec!["two", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let paths = auto_import_module_candidates(&db, importing, "math", "value") + .into_iter() + .map(|candidate| candidate.import_path) + .collect::>(); + assert_eq!(paths, ["lib.two.math"]); + + let (db, entry) = load_sources([ + (vec!["main"], "enum one { One } function main() {}"), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); + + let (db, entry) = load_sources([ + ( + vec!["main"], + "import {one} from lib.names; function main() {}", + ), + ( + vec!["names"], + "export { one }; function one() returns (word) { return 0; }", + ), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); +} + +#[test] +fn module_auto_imports_check_contract_local_prefix_bindings() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "contract C { + one: word; + enum two { Two } + function three() returns (word) { return 0; } + function main() {} + }", + ), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ( + vec!["two", "math"], + "export { value }; function value() returns (word) { return 2; }", + ), + ( + vec!["three", "math"], + "export { value }; function value() returns (word) { return 3; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + assert!(auto_import_module_candidates(&db, importing, "math", "value").is_empty()); +} + +#[test] +fn namespace_aliases_do_not_reserve_source_path_prefixes() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as deep from lib.one.deep; function main() {}", + ), + ( + vec!["one", "deep"], + "export { old }; function old() returns (word) { return 0; }", + ), + ( + vec!["one", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let candidates = auto_import_module_candidates(&db, importing, "math", "value"); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.one.math"); + + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as deep from lib.missing.deep; function main() {}", + ), + ( + vec!["missing", "math"], + "export { value }; function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let candidates = auto_import_module_candidates(&db, importing, "math", "value"); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.missing.math"); +} + +#[test] +fn namespace_qualified_lookup_yields_to_value_receivers() { + let (db, entry) = load_sources([ + ( + vec!["main"], + r#" +import * as p from lib; + +struct Pair { + x: word; +} + +function project(p: Pair) returns (word) { + return p.x; +} + +contract C { + p: Pair; + + function readContract() returns (word) { + return p.x; + } +} + +function readImport() returns (word) { + return p.x(); +} +"#, + ), + ( + vec!["lib"], + r#" +export { x }; + +function x() returns (word) { + return 1; +} +"#, + ), + ]); + let main_file = db.module_files[&entry]; + let main_hir = parse_file_to_hir(&db, main_file).module(&db); + let main_module = module_id_from_key(&db, &entry); + let env = module_env(&db, main_module); + let scope = env.item_scope.clone().expect("main item scope"); + let resolution = resolve_module_with_imports(&db, main_hir, scope, &env); + assert!( + resolution.diagnostics.is_empty(), + "unexpected name-resolution diagnostics: {:#?}", + resolution.diagnostics + ); + + let project = named_function_body(&db, main_hir, "project"); + let (project_field, project_base) = named_field_access(&db, project, "p", "x"); + let project_map = body_resolution_map(&resolution, project); + assert!(matches!( + expression_resolution(project_map, project, project_base), + Some(Resolution::Param(_)) + )); + assert_eq!( + expression_resolution(project_map, project, project_field), + None, + "a named ADT field must remain unresolved for type inference" + ); + + let read_contract = named_function_body(&db, main_hir, "readContract"); + let (contract_field, contract_base) = named_field_access(&db, read_contract, "p", "x"); + let contract_map = body_resolution_map(&resolution, read_contract); + assert!(matches!( + expression_resolution(contract_map, read_contract, contract_base), + Some(Resolution::Field(_)) + )); + assert_eq!( + expression_resolution(contract_map, read_contract, contract_field), + None, + "a contract field receiver must not be reinterpreted as a module alias" + ); + + let read_import = named_function_body(&db, main_hir, "readImport"); + let (import_field, import_base) = named_field_access(&db, read_import, "p", "x"); + let import_map = body_resolution_map(&resolution, read_import); + assert!(matches!( + expression_resolution(import_map, read_import, import_base), + Some(Resolution::Module(_)) + )); + assert!(matches!( + expression_resolution(import_map, read_import, import_field), + Some(Resolution::Def { + kind: DefResolutionKind::Function, + .. + }) + )); +} + +#[test] +fn module_auto_imports_allow_a_separate_plain_import_after_a_selective_import() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import {other} from lib.one.math; function main() {}", + ), + ( + vec!["one", "math"], + "export { other, value }; function other() returns (word) { return 0; } function value() returns (word) { return 1; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let candidates = auto_import_module_candidates(&db, importing, "math", "value"); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.one.math"); +} + +#[test] +fn auto_imports_exclude_namespace_blind_selector_collisions_within_one_provider() { + let (db, entry) = load_sources([ + (vec!["main"], "function main() {}"), + ( + vec!["provider"], + "export { Shared, term_only, TypeOnly }; + function Shared() returns (word) { return 1; } + enum Shared { Shared } + function term_only() returns (word) { return 2; } + enum TypeOnly { TypeOnly }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + let provider = module_id_from_key(&db, &module_key(["provider"])); + let interface = public_interface(&db, provider); + assert!(interface.terms.contains_key("Shared")); + assert!(interface.types.contains_key("Shared")); + + assert!(auto_import_candidates(&db, importing, "Shared", Namespace::Term).is_empty()); + assert!(auto_import_candidates(&db, importing, "Shared", Namespace::Type).is_empty()); + + let term_candidates = auto_import_candidates(&db, importing, "term_only", Namespace::Term); + assert_eq!(term_candidates.len(), 1); + assert_eq!(term_candidates[0].import_path, "lib.provider"); + let type_candidates = auto_import_candidates(&db, importing, "TypeOnly", Namespace::Type); + assert_eq!(type_candidates.len(), 1); + assert_eq!(type_candidates[0].import_path, "lib.provider"); +} + +#[test] +fn auto_imports_suppress_different_target_for_explicit_selector_but_keep_same_target() { + let (db, entry) = load_sources([ + (vec!["main"], "import {Foo} from lib.a; function main() {}"), + ( + vec!["a"], + "export { Foo }; function Foo() returns (word) { return 1; }", + ), + ( + vec!["b"], + "export { Foo }; function Foo() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + + let candidates = auto_import_candidates(&db, importing, "Foo", Namespace::Term); + let paths = candidates + .iter() + .map(|candidate| candidate.import_path.as_str()) + .collect::>(); + assert_eq!(paths, ["lib.a"]); +} + +#[test] +fn auto_imports_consider_selector_aliases_by_their_local_name() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import {Original as Foo} from lib.a; function main() {}", + ), + ( + vec!["a"], + "export { Original }; function Original() returns (word) { return 1; }", + ), + ( + vec!["b"], + "export { Foo }; function Foo() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + + assert!(auto_import_candidates(&db, importing, "Foo", Namespace::Term).is_empty()); +} + +#[test] +fn auto_imports_consider_bindings_from_selective_imports() { + let (db, entry) = load_sources([ + (vec!["main"], "import {Foo} from lib.a; function main() {}"), + ( + vec!["a"], + "export { Foo }; function Foo() returns (word) { return 1; }", + ), + ( + vec!["b"], + "export { Foo }; function Foo() returns (word) { return 2; }", + ), + ]); + let importing = module_id_from_key(&db, &entry); + + let candidates = auto_import_candidates(&db, importing, "Foo", Namespace::Term); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.a"); +} + +#[test] +fn auto_imports_suppress_cross_namespace_collisions_from_different_targets() { + let (db, entry) = load_sources([ + (vec!["main"], "import {Foo} from lib.a; function main() {}"), + ( + vec!["a"], + "export { Foo }; function Foo() returns (word) { return 1; }", + ), + (vec!["b"], "export { Foo }; enum Foo { Foo }"), + ]); + let importing = module_id_from_key(&db, &entry); + + assert!(auto_import_candidates(&db, importing, "Foo", Namespace::Type).is_empty()); +} + +#[test] +fn auto_imports_keep_main_workspace_namespaces_isolated() { + let workspace_a = "1111111111111111"; + let workspace_b = "2222222222222222"; + let detached = "3333333333333333"; + let (db, entry) = load_sources([ + ( + vec!["__solcore_workspace__", workspace_a, "main"], + "function main() {}", + ), + ( + vec!["__solcore_workspace__", workspace_a, "nested", "util"], + "export { wanted }; function wanted() returns (word) { return 1; }", + ), + ( + vec!["__solcore_workspace__", workspace_b, "nested", "util"], + "export { wanted }; function wanted() returns (word) { return 2; }", + ), + ( + vec!["__solcore_detached__", detached, "main"], + "function main() {}", + ), + ( + vec!["__solcore_detached__", detached, "nested", "util"], + "export { wanted }; function wanted() returns (word) { return 3; }", + ), + ]); + let importing = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec![ + "__solcore_workspace__".to_owned(), + workspace_a.to_owned(), + "main".to_owned(), + ], + }, + ); + // `load_sources` always reports `main` as its convenience entry; make sure + // this test does not accidentally exercise that unrelated synthetic key. + assert_ne!(importing, module_id_from_key(&db, &entry)); + + let candidates = auto_import_candidates(&db, importing, "wanted", Namespace::Term); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].import_path, "lib.nested.util"); + assert_eq!( + candidates[0].provider.logical_path(&db), + &[ + "__solcore_workspace__".to_owned(), + workspace_a.to_owned(), + "nested".to_owned(), + "util".to_owned(), + ] + ); + assert!(!candidates[0].import_path.contains("__solcore_workspace__")); + + let module_candidates = auto_import_module_candidates(&db, importing, "util", "wanted"); + assert_eq!(module_candidates.len(), 1); + assert_eq!(module_candidates[0].import_path, "lib.nested.util"); + assert_eq!(module_candidates[0].provider, candidates[0].provider); + + let detached_importing = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec![ + "__solcore_detached__".to_owned(), + detached.to_owned(), + "main".to_owned(), + ], + }, + ); + let detached_candidates = + auto_import_candidates(&db, detached_importing, "wanted", Namespace::Term); + assert_eq!(detached_candidates.len(), 1); + assert_eq!(detached_candidates[0].import_path, "lib.nested.util"); + assert_eq!( + &detached_candidates[0].provider.logical_path(&db)[..2], + &["__solcore_detached__".to_owned(), detached.to_owned()] + ); +} + +#[test] +fn source_import_paths_use_canonical_library_syntax() { + let mut db = TestDb::default(); + let external_roots = BTreeMap::from([ + ("pkg".to_owned(), PathBuf::from("/memory/pkg")), + ("bad-name".to_owned(), PathBuf::from("/memory/bad-name")), + ]); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory/main"), + PathBuf::from("/memory/std"), + external_roots, + )); + db.module_fs_snapshot = Some(ModuleFsSnapshot::new(&db, BTreeSet::new(), BTreeMap::new())); + let keys = [ + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }, + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["nested".to_owned(), "util".to_owned()], + }, + ModuleKey { + library: LibraryId::Std, + logical_path: vec!["collections".to_owned(), "list".to_owned()], + }, + ModuleKey { + library: LibraryId::External("pkg".to_owned()), + logical_path: vec!["math".to_owned(), "api".to_owned()], + }, + ]; + let sources = [ + "function main() {}", + "function local_only() {}", + "export { std_value }; function std_value() returns (word) { return 1; }", + "export { external_value }; function external_value() returns (word) { return 2; }", + ]; + for (key, source) in keys.iter().zip(sources) { + let file = SourceFile::new(&db, fixture_url(key), Some(source.to_owned())); + db.insert_module_file(key.clone(), file); + } + let modules = keys + .iter() + .map(|key| module_id_from_key(&db, key)) + .collect::>(); + + assert_eq!(source_import_path(&db, modules[0], modules[0]), None); + assert_eq!( + source_import_path(&db, modules[0], modules[1]).as_deref(), + Some("lib.nested.util") + ); + assert_eq!( + source_import_path(&db, modules[0], modules[2]).as_deref(), + Some("std.collections.list") + ); + assert_eq!( + source_import_path(&db, modules[0], modules[3]).as_deref(), + Some("@pkg.math.api") + ); + for (index, (path, expected)) in [ + ("lib.nested.util", modules[1]), + ("std.collections.list", modules[2]), + ("@pkg.math.api", modules[3]), + ] + .into_iter() + .enumerate() + { + let file = SourceFile::new( + &db, + format!("memory:///roundtrip-{index}.solc") + .parse() + .expect("round-trip test URL"), + Some(format!("import {path};")), + ); + let import_ref = module_imports(&db, file) + .import_refs + .into_iter() + .next() + .expect("generated path parses as an import"); + assert_eq!( + resolve_module_path_candidate(&db, modules[0], &import_ref) + .expect("generated path resolves") + .module, + expected + ); + } + + let invalid_main = ModuleId::new(&db, LibraryId::Main, vec!["bad.path".to_owned()]); + let invalid_std = ModuleId::new(&db, LibraryId::Std, vec!["bad-name".to_owned()]); + let invalid_external = ModuleId::new( + &db, + LibraryId::External("bad-name".to_owned()), + vec!["api".to_owned()], + ); + assert_eq!(source_import_path(&db, modules[0], invalid_main), None); + assert_eq!(source_import_path(&db, modules[0], invalid_std), None); + assert_eq!(source_import_path(&db, modules[0], invalid_external), None); + assert_eq!( + auto_import_candidates(&db, modules[0], "std_value", Namespace::Term)[0].import_path, + "std.collections.list" + ); + assert_eq!( + auto_import_candidates(&db, modules[0], "external_value", Namespace::Term)[0].import_path, + "@pkg.math.api" + ); + assert_eq!( + auto_import_module_candidates(&db, modules[0], "list", "std_value")[0].import_path, + "std.collections.list" + ); + assert_eq!( + auto_import_module_candidates(&db, modules[0], "api", "external_value")[0].import_path, + "@pkg.math.api" + ); +} + +#[test] +fn std_subpath_falls_back_to_local_module_when_std_module_is_missing() { + let fixture = fixture_dir("ok/local_std_subpath"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (graph, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + + let local = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec!["std".to_owned(), "a".to_owned(), "b".to_owned()], + }, + ); + assert!(graph.modules.contains(&local)); + let interface = public_interface(&db, local); + assert!(interface.terms.contains_key("value")); +} + +#[test] +fn import_and_export_module_aliases_are_public_bindings() { + let fixture = fixture_dir("ok/alias"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + + let main = module_id_from_key(&db, &entry); + let interface = public_interface(&db, main); + let target = interface + .module_aliases + .get("PublicUtil") + .expect("exported module alias"); + assert_eq!(target.display(&db), "util"); +} + +#[test] +fn reexport_chain_exposes_remote_origin() { + let fixture = fixture_dir("ok/reexport_chain"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + + let b = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec!["b".to_owned()], + }, + ); + let interface = public_interface(&db, b); + let origin = interface.terms.get("value").expect("re-exported value"); + assert_eq!(origin.module.display(&db), "a"); +} + +#[test] +fn plain_import_binds_reexported_terms() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import lib.wrapper;\n\ + function main() returns (word) { return renamed(); }", + ), + ( + vec!["base"], + "export { original };\n\ + function original() returns (word) { return 0; }", + ), + ( + vec!["wrapper"], + "import {original as renamed} from lib.base;\n\ + export { renamed };", + ), + ]); + + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); +} + +#[test] +fn private_library_members_do_not_leak_through_import_aliases_or_reexports() { + let (db, entry) = load_sources([ + ( + vec!["main"], + r#" +import {Helpers as Direct} from lib.base; +import * as base_ns from lib.base; +import {Renamed as ViaReexport} from lib.wrapper; + +function direct() returns (word) { return Direct.reveal(); } +function namespaced() returns (word) { return base_ns.Helpers.reveal(); } +function reexported() returns (word) { return ViaReexport.reveal(); } +"#, + ), + ( + vec!["base"], + r#" +export { Helpers }; + +library Helpers { + function secret() private pure returns (word) { return 7; } + function reveal() internal pure returns (word) { return secret(); } +} +"#, + ), + ( + vec!["wrapper"], + r#" +import {Helpers as Renamed} from lib.base; +export { Renamed }; +"#, + ), + ]); + + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + + let main = module_id_from_key(&db, &entry); + let env = module_env(&db, main); + for visible in [ + "Direct.reveal", + "base_ns.Helpers.reveal", + "ViaReexport.reveal", + ] { + assert!(env.terms.contains_key(visible), "missing {visible}"); + } + for hidden in [ + "Direct.secret", + "base_ns.Helpers.secret", + "ViaReexport.secret", + ] { + assert!(!env.terms.contains_key(hidden), "leaked {hidden}"); + } +} + +#[test] +fn recursive_export_cycle_reaches_fixed_point() { + let fixture = fixture_dir("ok/cycle"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (graph, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); + assert!( + strongly_connected_components(&graph) + .iter() + .any(|component| component.len() == 2), + "expected a two-module SCC over export references" + ); + + let a = module_id_from_key( + &db, + &ModuleKey { + library: LibraryId::Main, + logical_path: vec!["a".to_owned()], + }, + ); + let interface = public_interface(&db, a); + assert!(interface.terms.contains_key("fa")); + assert!(interface.terms.contains_key("fb")); +} + +#[test] +fn external_library_import_uses_configured_root() { + let fixture = fixture_dir("ok/external"); + let mut external_roots = BTreeMap::new(); + external_roots.insert("pkg".to_owned(), fixture.join("extroot")); + let (db, entry) = load_fixture(&fixture, external_roots); + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); +} + +#[test] +fn wildcard_hiding_validates_against_source_interface() { + let fixture = fixture_dir("ok/selective_hiding"); + let (db, entry) = load_fixture(&fixture, BTreeMap::new()); + let (_, diagnostics) = run(&db, &entry); + assert_no_diagnostics(&db, &diagnostics); +} + +#[test] +fn parse_broken_selected_import_does_not_blame_importer() { + let (db, entry) = load_sources(parse_broken_provider_sources( + "import {lost} from util; + function main() returns (word) { return lost(0); }", + )); + let main = module_id_from_key(&db, &entry); + assert_eq!(module_diagnostic_codes(&db, main), Vec::::new()); + + let util = module_id_from_key(&db, &module_key(["util"])); + let util_diagnostics = lowered_module_diagnostics(&db, util); + assert!(!util_diagnostics.is_empty()); + assert_eq!( + diagnostic_codes(&util_diagnostics), + vec!["SC0001".to_owned()] + ); +} + +#[test] +fn parse_broken_qualified_import_does_not_blame_importer() { + let (db, entry) = load_sources(parse_broken_provider_sources( + "import * as util from util; + function main() returns (word) { return util.lost(0); }", + )); + let main = module_id_from_key(&db, &entry); + assert_eq!(module_diagnostic_codes(&db, main), Vec::::new()); +} + +#[test] +fn parse_broken_leaf_does_not_mark_unrelated_module_prefixes_incomplete() { + let (db, entry) = load_sources([ + ( + vec!["main"], + "import * as c from lib.a.b.c; import * as x from lib.a.x; function main() returns (word) { return a.missing(); }", + ), + ( + vec!["a", "b", "c"], + "function value() returns (word) { let broken = ; return 1; }", + ), + ( + vec!["a", "x"], + "function other() returns (word) { return 2; }", + ), + ]); + let main = module_id_from_key(&db, &entry); + let leaf = module_id_from_key(&db, &module_key(["a", "b", "c"])); + let sibling = module_id_from_key(&db, &module_key(["a", "x"])); + let env = module_env(&db, main); + + assert_eq!(env.surface.modules.get("c"), Some(&leaf)); + assert_eq!(env.surface.modules.get("x"), Some(&sibling)); + assert!(!env.surface.modules.contains_key("a")); + assert!(!env.surface.modules.contains_key("a.b")); + assert!(!env.surface.modules.contains_key("a.b.c")); + assert!(!env.surface.modules.contains_key("a.x")); + assert!(!env.surface.module_qualifiers.contains("a")); + assert!(!env.surface.module_qualifiers.contains("a.b")); + assert!(env.surface.incomplete_modules.contains("c")); + assert!(!env.surface.incomplete_modules.contains("x")); + assert!(!env.surface.incomplete_modules.contains("a")); + assert!(!env.surface.incomplete_modules.contains("a.b")); + assert!( + module_diagnostic_codes(&db, main) + .iter() + .any(|code| code == "SC0101"), + "undefined prefix member should not be suppressed" + ); +} + +#[test] +fn parse_broken_module_diagnostics_publish_only_parse_errors() { + let (db, entry) = load_sources([( + vec!["main"], + "function main() returns (word) { + let x = ; + return missing; + }", + )]); + let main = module_id_from_key(&db, &entry); + let diagnostics = lowered_module_diagnostics(&db, main); + assert!(!diagnostics.is_empty()); + assert_eq!(diagnostic_codes(&diagnostics), vec!["SC0001".to_owned()]); +} + +#[test] +fn imports_corpus_matches_reference_expectations() { + std::thread::Builder::new() + .name("imports-corpus-validation".to_owned()) + .stack_size(64 * 1024 * 1024) + .spawn(imports_corpus_matches_reference_expectations_impl) + .expect("spawn corpus validation") + .join() + .expect("corpus validation thread"); +} + +fn imports_corpus_matches_reference_expectations_impl() { + let root = parser_corpus_imports_dir(); + let mut external_roots = BTreeMap::new(); + external_roots.insert("extlib".to_owned(), root.join("extlib")); + + let mut expected_pass_total = 0usize; + let mut expected_pass_passing = 0usize; + let mut expected_fail_total = 0usize; + let mut expected_fail_failing = 0usize; + let mut divergences = Vec::new(); + let mut mismatches = Vec::new(); + + for case in IMPORT_CORPUS_CASES { + let path = root.join(case.path); + if !path.exists() { + continue; + } + let (db, entry) = load_entry(&root, &path, external_roots.clone()); + let (_, diagnostics) = run(&db, &entry); + let actual_failed = !diagnostics.is_empty(); + let expected_failed = case.expected_failure; + + if expected_failed { + expected_fail_total += 1; + expected_fail_failing += usize::from(actual_failed); + } else { + expected_pass_total += 1; + expected_pass_passing += usize::from(!actual_failed); + } + + if actual_failed != expected_failed { + if let Some(divergence) = known_divergence(case.path) { + divergences.push(format!("{}: {}", case.path, divergence.reason)); + } else { + mismatches.push(format!( + "{} expected {} but got {} diagnostics: {:?}", + case.path, + if expected_failed { + "failure" + } else { + "success" + }, + diagnostics.len(), + diagnostics + .iter() + .filter_map(|diagnostic| diagnostic.code.as_deref()) + .collect::>() + )); + } + } + } + + println!( + "imports corpus scoreboard: {expected_pass_passing}/{expected_pass_total} expected-pass passing; {expected_fail_failing}/{expected_fail_total} expected-fail failing; {} known divergences", + divergences.len() + ); + for divergence in &divergences { + println!("known divergence: {divergence}"); + } + + assert!( + mismatches.is_empty(), + "imports corpus verdict mismatches:\n{}", + mismatches.join("\n") + ); +} + +fn run<'db>(db: &'db TestDb, entry: &ModuleKey) -> (ModuleGraph<'db>, Vec) { + let entry = module_id_from_key(db, entry); + let graph = resolve_reachable_full(db, entry); + let mut diagnostics = reachable_diagnostics(db, entry) + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect::>(); + sort_dedup_diagnostics(db, &mut diagnostics); + (graph, diagnostics) +} + +fn load_fixture(root: &Path, external_roots: BTreeMap) -> (TestDb, ModuleKey) { + let mut db = TestDb::default(); + let std_root = repo_std_dir(); + db.module_tree = Some(ModuleTree::new( + &db, + root.to_path_buf(), + std_root.clone(), + external_roots.clone(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + &db, + std::iter::once(root) + .chain(std::iter::once(std_root.as_path())) + .chain(external_roots.values().map(|path| path.as_path())), + )); + load_library_files(&mut db, LibraryId::Main, root, root); + for (name, external_root) in external_roots { + load_library_files( + &mut db, + LibraryId::External(name), + &external_root, + &external_root, + ); + } + + let entry_path = root.join("main.solc"); + let entry_key = module_key_for_path(LibraryId::Main, root, &entry_path).expect("entry key"); + (db, entry_key) +} + +fn load_sources(sources: [(Vec<&str>, &str); N]) -> (TestDb, ModuleKey) { + let mut db = TestDb::default(); + let std_root = repo_std_dir(); + db.module_tree = Some(ModuleTree::new( + &db, + PathBuf::from("/memory/main"), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + &db, + std::iter::once(std_root.as_path()), + )); + for (path, source) in sources { + let key = ModuleKey { + library: LibraryId::Main, + logical_path: path.into_iter().map(str::to_owned).collect(), + }; + let url = fixture_url(&key); + let file = SourceFile::new(&db, url, Some(source.to_owned())); + db.insert_module_file(key, file); + } + ( + db, + ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }, + ) +} + +fn parse_broken_provider_sources(main: &str) -> [(Vec<&str>, &str); 2] { + [ + (vec!["main"], main), + ( + vec!["util"], + "lost(x: word) returns (word) { return 0; } + function other() {}", + ), + ] +} + +fn module_key(path: [&str; N]) -> ModuleKey { + ModuleKey { + library: LibraryId::Main, + logical_path: path.into_iter().map(str::to_owned).collect(), + } +} + +fn named_function_body<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FuncBody<'db> { + fn from_items<'db>( + db: &'db TestDb, + items: impl IntoIterator>, + name: &str, + ) -> Option> { + for item in items { + match item { + Item::FunctionDef(function) if (*function.sig(db).name.atom()).text(db) == name => { + return function.body(db); + } + Item::ContractDef(contract) => { + for member in contract.items(db) { + if let ContractItem::FunctionDef(function) = member + && (*function.sig(db).name.atom()).text(db) == name + { + return function.body(db); + } + } + } + _ => {} + } + } + None + } + + from_items(db, module.items(db).iter().copied(), name).expect("named function body") +} + +fn named_field_access<'db>( + db: &'db TestDb, + body: FuncBody<'db>, + receiver: &str, + field: &str, +) -> (Id>, Id>) { + body.exprs(db) + .iter() + .find_map(|(expr_id, expr)| { + let ExprKind::Field { + base, + field: selected, + } = &expr.kind + else { + return None; + }; + let ExprKind::Ident(base_name) = &body.exprs(db).get(*base).kind else { + return None; + }; + ((*base_name.atom()).text(db) == receiver && (*selected.atom()).text(db) == field) + .then_some((expr_id, *base)) + }) + .expect("named field access") +} + +fn body_resolution_map<'a, 'db>( + resolution: &'a hir::nameres::ModuleResolutionMap<'db>, + body: FuncBody<'db>, +) -> &'a hir::nameres::BodyResolutionMap<'db> { + resolution + .bodies + .iter() + .find(|map| map.exprs.iter().any(|entry| entry.body == body)) + .expect("body resolution map") +} + +fn expression_resolution<'a, 'db>( + map: &'a hir::nameres::BodyResolutionMap<'db>, + body: FuncBody<'db>, + expr: Id>, +) -> Option<&'a Resolution<'db>> { + map.exprs + .iter() + .find(|entry| entry.body == body && entry.expr == expr) + .map(|entry| &entry.resolution) +} + +fn lowered_module_diagnostics<'db>(db: &'db TestDb, module: ModuleId<'db>) -> Vec { + module_diagnostics(db, module) + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect() +} + +fn module_diagnostic_codes(db: &TestDb, module: ModuleId<'_>) -> Vec { + diagnostic_codes(&lowered_module_diagnostics(db, module)) +} + +fn diagnostic_codes(diagnostics: &[Diagnostic]) -> Vec { + diagnostics + .iter() + .filter_map(|diagnostic| diagnostic.code.clone()) + .collect() +} + +fn load_entry( + root: &Path, + entry_path: &Path, + external_roots: BTreeMap, +) -> (TestDb, ModuleKey) { + let mut db = TestDb::default(); + let std_root = repo_std_dir(); + db.module_tree = Some(ModuleTree::new( + &db, + root.to_path_buf(), + std_root.clone(), + external_roots.clone(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + &db, + std::iter::once(root) + .chain(std::iter::once(std_root.as_path())) + .chain(external_roots.values().map(|path| path.as_path())), + )); + let entry_key = module_key_for_path(LibraryId::Main, root, entry_path).expect("entry key"); + let entry_file = source_file_for_path(&db, entry_path); + db.insert_module_file(entry_key.clone(), entry_file); + load_reachable_modules(&mut db, entry_key.clone()); + (db, entry_key) +} + +fn load_reachable_modules(db: &mut TestDb, entry: ModuleKey) { + let mut queue = vec![entry]; + let mut visited = FxHashSet::default(); + + while let Some(key) = queue.pop() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = db.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = solcore_nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map(|path| { + let resolved = resolve_module_path_candidate(&*db, module, &path).ok()?; + Some((resolved.module.key(&*db), resolved.file_path)) + }) + .collect::>() + }; + + for (target_key, file_path) in targets { + if !db.module_files.contains_key(&target_key) && file_path.exists() { + let file = source_file_for_path(db, &file_path); + db.insert_module_file(target_key.clone(), file); + } + if db.module_files.contains_key(&target_key) { + queue.push(target_key); + } + } + } +} + +fn source_file_for_path(db: &TestDb, path: &Path) -> SourceFile { + let source = fs::read_to_string(path).expect("source file"); + let url = Url::from_file_path(path).expect("file URL"); + SourceFile::new(db, url, Some(source)) +} + +fn module_fs_snapshot_for_roots<'a>( + db: &TestDb, + roots: impl IntoIterator, +) -> ModuleFsSnapshot { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +fn load_library_files(db: &mut TestDb, library: LibraryId, root: &Path, dir: &Path) { + for entry in fs::read_dir(dir).expect("read fixture directory") { + let path = entry.expect("fixture entry").path(); + if path.is_dir() { + load_library_files(db, library.clone(), root, &path); + } else if path.extension().and_then(|ext| ext.to_str()) == Some("solc") { + let key = module_key_for_path(library.clone(), root, &path).expect("module key"); + let source = fs::read_to_string(&path).expect("fixture source"); + let url = fixture_url(&key); + let file = SourceFile::new(db, url, Some(source)); + db.insert_module_file(key, file); + } + } +} + +fn fixture_url(key: &ModuleKey) -> Url { + let library = match &key.library { + LibraryId::Main => "main".to_owned(), + LibraryId::Std => "std".to_owned(), + LibraryId::External(name) => format!("external/{name}"), + }; + let path = key.logical_path.join("/"); + format!("memory:///{library}/{path}.solc") + .parse() + .expect("fixture memory URL") +} + +fn assert_no_diagnostics(db: &TestDb, diagnostics: &[Diagnostic]) { + assert!( + diagnostics.is_empty(), + "expected no diagnostics\n{}", + render_diagnostics(db, diagnostics) + ); +} + +fn render_diagnostics(db: &dyn hir::Db, diagnostics: &[Diagnostic]) -> String { + if diagnostics.is_empty() { + return "no diagnostics\n".to_owned(); + } + + let renderer = Renderer::plain(); + let mut output = String::new(); + for (idx, diagnostic) in diagnostics.iter().enumerate() { + if idx > 0 { + output.push_str("\n---\n\n"); + } + output.push_str(&diagnostic.render_with(db, &renderer)); + } + output +} + +fn sort_dedup_diagnostics(db: &dyn hir::Db, diagnostics: &mut Vec) { + sort_dedup_rendered_diagnostics(db, diagnostics); +} + +fn fixture_dir(relative: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(relative) +} + +fn parser_corpus_imports_dir() -> PathBuf { + repo_root() + .join("crates") + .join("parser") + .join("tests") + .join("fixtures") + .join("corpus") + .join("ok") + .join("test") + .join("imports") +} + +fn repo_std_dir() -> PathBuf { + repo_root().join("std") +} + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("nameres crate lives under /crates/nameres") + .to_path_buf() +} + +#[derive(Clone, Copy)] +struct ImportCorpusCase { + path: &'static str, + expected_failure: bool, +} + +#[derive(Clone, Copy)] +struct KnownDivergence { + path: &'static str, + reason: &'static str, +} + +fn known_divergence(path: &str) -> Option { + KNOWN_DIVERGENCES + .iter() + .copied() + .find(|divergence| divergence.path == path) +} + +const KNOWN_DIVERGENCES: &[KnownDivergence] = &[ + KnownDivergence { + path: "hidden_ctor_nonexhaustive_fail.solc", + reason: "reference fails later exhaustiveness checking for partial constructor visibility; Rust nameres records partial-data metadata but does not run exhaustiveness", + }, + KnownDivergence { + path: "symlink_identity_fail.solc", + reason: "reference rejects distinct module identities for equivalent helper sources; Rust nameres does not canonicalize/symlink-check type identity in this pass", + }, + KnownDivergence { + path: "private_bad_main.solc", + reason: "reference type-checks private helper bodies and rejects the unexported broken function; Rust nameres intentionally reports only name-resolution diagnostics", + }, + KnownDivergence { + path: "pragma_scope_main.solc", + reason: "reference fails pragma-scoped typeclass/termination validation; Rust nameres does not implement that semantic check", + }, +]; + +const IMPORT_CORPUS_CASES: &[ImportCorpusCase] = &[ + ImportCorpusCase { + path: "booldef.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolmain.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "unordered_imports_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolalias.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "alias_hides_original_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "boolalias_open_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "boolqualified.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolqualifiedtype.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolaliastype.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "module_unqualified_fun_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "alias_unqualified_fun_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "module_unqualified_type_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "alias_unqualified_type_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "module_unqualified_constr_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "alias_unqualified_constr_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "selective_unqualified_fun_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "transitive_dep_main_module.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "transitive_dep_main_select.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "opaque_alias_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "opaque_select_alias_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "opaque_alias_leak_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "opaque_alias_qualifier_leak_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "opaque_select_direct_leak_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "module_name_shadow.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "wrapper_shadow_success.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "ns_cross_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "ns_constr_dup.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "strict_open_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "boolselect.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolconselect_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "boolconselect_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "nested_alias.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "nested_select.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "nested_foo_and_bar.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "nested_direct_qualifier.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "nested_deep_qualifier.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_import_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_import_mixed.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_import_hiding.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_hiding_amb_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_import_dup.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_export_mixed.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "glob_amb_main_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "glob_import_hiding_unknown_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "select_hiding_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "select_hiding_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "export_item_dup_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "export_module_dup_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "select_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "select_shadow_local.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "select_shadow_param_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "select_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "select_unknown.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "select_dup_item.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "alias_dup.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "amb_main.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "amb_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "dupqual_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "dupqual_module_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "private_helper_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "module_qualified_constructor.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "module_qualified_constructor_pattern.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "module_qualified_constructor_alias.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "type_collision_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "dot_context_expr.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_items_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_select_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_select_alias_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_module_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_module_alias_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_ctor_pattern.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_ctor_expr_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "reexport_ctor_expr_hidden_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "reexport_ctor_hidden_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "hidden_ctor_expr_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "hidden_ctor_dot_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "hidden_ctor_pattern_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "hidden_ctor_nonexhaustive_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "hidden_ctor_wildcard_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "rootcheck/nested/main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "rootcheck/nested/relative_and_lib_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "external_lib_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "external_lib_alias_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "import_std_minimal.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "select_alias_item_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "select_alias_multi_ok.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "external_lib_missing_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "symlink_identity_fail.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "private_bad_main.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "pragma_scope_main.solc", + expected_failure: true, + }, + ImportCorpusCase { + path: "selfcycle.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "cycle_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "wild_main.solc", + expected_failure: false, + }, + ImportCorpusCase { + path: "leak_main.solc", + expected_failure: true, + }, +]; diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index 3313b664..8315eb17 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -2,14 +2,17 @@ name = "solcore-parser" version = "0.1.0" edition.workspace = true +rust-version.workspace = true [dependencies] salsa = { workspace = true } hir = { workspace = true } chumsky = "0.12" logos = "0.16" +tracing = { workspace = true } [dev-dependencies] dir-test = "0.4.1" insta = "1.43.2" annotate-snippets = { workspace = true } +proptest = { workspace = true } diff --git a/crates/parser/src/lexer.rs b/crates/parser/src/lexer.rs index ed4a405a..3a7392d6 100644 --- a/crates/parser/src/lexer.rs +++ b/crates/parser/src/lexer.rs @@ -1,152 +1,350 @@ +//! Lexical tokens for the Solcore parser. +//! +//! Logos produces token spans in absolute byte offsets over the input string. +//! Comments and whitespace are skipped; invalid characters are reported by the +//! parser's tokenization wrapper so the rest of the grammar can recover. + +use std::ops::Range; + use logos::Logos; +/// Additional state collected while lexing. +/// +/// Comments remain skipped tokens as far as the parser grammar is concerned, +/// but their source ranges are retained here for declaration trivia lowering. +#[doc(hidden)] +#[derive(Debug, Default)] +pub struct LexerExtras { + pub(crate) comments: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum LexedCommentKind { + Line, + Block, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct LexedComment { + pub(crate) kind: LexedCommentKind, + pub(crate) range: Range, +} + +/// Lexer error kind. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum LexError { + /// Generic invalid token. + #[default] + Invalid, + /// A block comment reached end of file before its matching terminator. + UnterminatedBlockComment, + /// A string literal used a backslash escape not supported by the language. + InvalidStringEscape, +} + +/// Token recognized by the Solcore lexer. +/// +/// Literal and identifier variants borrow slices from the input source. Token +/// ordering matters for overlapping operators: multi-character operators are +/// defined before their single-character prefixes. #[derive(Logos, Debug, Clone, PartialEq)] #[logos(skip r"[ \t\n\r\f]+")] +#[logos(error = LexError)] +#[logos(extras = LexerExtras)] pub enum Token<'a> { - // Keywords. + /// `contract`. #[token("contract")] Contract, + /// `interface`. + #[token("interface")] + Interface, + /// `library`. + #[token("library")] + Library, + /// `import`. #[token("import")] Import, + /// `from`. + #[token("from")] + From, + /// `export`. + #[token("export")] + Export, + /// `as`. + #[token("as")] + As, + /// `let`. #[token("let")] Let, - #[token("data")] - Data, - #[token("class")] - Class, - #[token("forall")] - Forall, - #[token("instance")] - Instance, + /// `comptime`. + #[token("comptime")] + Comptime, + /// `enum`. + #[token("enum")] + Enum, + /// `struct`. + #[token("struct")] + Struct, + /// `trait`. + #[token("trait")] + Trait, + /// `impl`. + #[token("impl")] + Impl, + /// `alias`. + #[token("alias")] + Alias, + /// `is`. + #[token("is")] + Is, + /// `where`. + #[token("where")] + Where, + /// `returns`. + #[token("returns")] + Returns, + /// `if`. #[token("if")] If, + /// `else`. #[token("else")] Else, + /// `for`. #[token("for")] For, + /// `while`. + #[token("while")] + While, + /// `unchecked`. + #[token("unchecked")] + Unchecked, + /// `switch`. #[token("switch")] Switch, + /// `type`. #[token("type")] Type, + /// `case`. #[token("case")] Case, + /// `default`. #[token("default")] Default, + /// `match`. #[token("match")] Match, + /// `public`. + #[token("public")] + Public, + /// `external`. + #[token("external")] + External, + /// `internal`. + #[token("internal")] + Internal, + /// `private`. + #[token("private")] + Private, + /// `pure`. + #[token("pure")] + Pure, + /// `view`. + #[token("view")] + View, + /// `payable`. + #[token("payable")] + Payable, + /// `function`. #[token("function")] Function, + /// `constructor`. #[token("constructor")] Constructor, + /// `fallback`. + #[token("fallback")] + Fallback, + /// `return`. #[token("return")] Return, + /// `revert`. + #[token("revert")] + Revert, + /// `leave`. #[token("leave")] Leave, + /// `continue`. #[token("continue")] Continue, + /// `break`. #[token("break")] Break, + /// `lam`. #[token("lam")] Lam, + /// `assembly`. #[token("assembly")] Assembly, + /// `pragma`. #[token("pragma")] Pragma, - #[token("then")] - Then, + /// `true`. #[token("true")] True, + /// `false`. #[token("false")] False, - // Multi-character operators (must be defined before single-character ones). + /// `:=`. #[token(":=")] ColonEq, + /// `->`. #[token("->")] Arrow, + /// `=>`. #[token("=>")] FatArrow, + /// `==`. #[token("==")] EqEq, + /// `!=`. #[token("!=")] NotEq, + /// `>=`. #[token(">=")] GreaterEq, + /// `<=`. #[token("<=")] LessEq, + /// `&&`. #[token("&&")] AndAnd, + /// `||`. #[token("||")] OrOr, + /// `+=`. #[token("+=")] PlusEq, + /// `-=`. #[token("-=")] MinusEq, - - // Single-character operators. + /// `^=`. + #[token("^=")] + CaretEq, + /// `&=`. + #[token("&=")] + AmpEq, + /// `|=`. + #[token("|=")] + PipeEq, + /// `%=`. + #[token("%=")] + PercentEq, + + /// `+`. #[token("+")] Plus, + /// `-`. #[token("-")] Minus, + /// `*`. #[token("*")] Star, + /// `/`. #[token("/")] Slash, + /// `%`. #[token("%")] Percent, + /// `!`. #[token("!")] Bang, + /// `~`. + #[token("~")] + Tilde, + /// `<`. #[token("<")] Less, + /// `>`. #[token(">")] Greater, + /// `=`. #[token("=")] Eq, + /// `|`. #[token("|")] Pipe, - - // Punctuation. + /// `&`. + #[token("&")] + Amp, + /// `^`. + #[token("^")] + Caret, + /// `@`. + #[token("@")] + At, + /// `?`. + #[token("?")] + Question, + + /// `.`. #[token(".")] Dot, + /// `:`. #[token(":")] Colon, + /// `;`. #[token(";")] Semi, + /// `,`. #[token(",")] Comma, + /// `(`. #[token("(")] LParen, + /// `)`. #[token(")")] RParen, + /// `{`. #[token("{")] LBrace, + /// `}`. #[token("}")] RBrace, + /// `[`. #[token("[")] LBracket, + /// `]`. #[token("]")] RBracket, + /// `_`. #[token("_")] Underscore, - // Literals. + /// Hexadecimal literal text. #[regex(r"0x[0-9a-fA-F]+", |lex| lex.slice())] HexLit(&'a str), + /// Decimal number literal text. #[regex(r"[0-9]+", |lex| lex.slice())] Number(&'a str), - #[regex(r#""([^"\\]|\\.)*""#, |lex| lex.slice())] + /// Quoted string literal text, including quotes and escapes. + #[regex(r#""([^"\\]|\\.)*""#, string_literal)] String(&'a str), - // Identifier (allows hyphens for pragma names like `no-bounded-variable-condition`). - #[regex(r"[a-zA-Z][a-zA-Z0-9_]*(-[a-zA-Z][a-zA-Z0-9_]*)*", |lex| lex.slice())] + /// Identifier or pragma-name text. + /// + /// The lexer accepts hyphens so pragma names such as + /// `no-bounded-variable-condition` tokenize as one item. The parser rejects + /// hyphenated text in normal identifier positions. + #[regex(r"\p{L}[\p{L}\p{N}_]*(-\p{L}[\p{L}\p{N}_]*)*", |lex| lex.slice())] Ident(&'a str), - // Comments (skipped). + /// Line comment skipped by the lexer. #[token("//", line_comment)] LineComment, + /// Block comment skipped by the lexer. #[token("/*", block_comment)] BlockComment, } @@ -157,39 +355,65 @@ fn line_comment<'a>(lex: &mut logos::Lexer<'a, Token<'a>>) -> logos::Skip { let remainder = lex.remainder(); let len = remainder.find('\n').unwrap_or(remainder.len()); lex.bump(len); + let range = lex.span(); + lex.extras.comments.push(LexedComment { + kind: LexedCommentKind::Line, + range, + }); logos::Skip } /// Skips a block comment starting with `/*` by consuming all characters until /// the matching `*/`. Supports nested block comments by tracking depth. -fn block_comment<'a>(lex: &mut logos::Lexer<'a, Token<'a>>) -> logos::Skip { +fn block_comment<'a>(lex: &mut logos::Lexer<'a, Token<'a>>) -> Result { let remainder = lex.remainder(); let mut depth = 1; - let mut chars = remainder.char_indices(); - - while let Some((i, c)) = chars.next() { - match c { - '*' => { - if let Some((_, '/')) = chars.next() { - depth -= 1; - if depth == 0 { - lex.bump(i + 2); - return logos::Skip; - } - } + let bytes = remainder.as_bytes(); + let mut i = 0; + + while i + 1 < bytes.len() { + match (bytes[i], bytes[i + 1]) { + (b'/', b'*') => { + depth += 1; + i += 2; } - '/' => { - if let Some((_, '*')) = chars.next() { - depth += 1; + (b'*', b'/') => { + depth -= 1; + i += 2; + if depth == 0 { + lex.bump(i); + let range = lex.span(); + lex.extras.comments.push(LexedComment { + kind: LexedCommentKind::Block, + range, + }); + return Ok(logos::Skip); } } - _ => {} + _ => i += 1, } } - // Unclosed comment, consume the rest. lex.bump(remainder.len()); - logos::Skip + Err(LexError::UnterminatedBlockComment) +} + +fn string_literal<'a>(lex: &mut logos::Lexer<'a, Token<'a>>) -> Result<&'a str, LexError> { + let slice = lex.slice(); + let mut chars = slice.chars(); + chars.next(); + while let Some(ch) = chars.next() { + if ch == '"' { + break; + } + if ch == '\\' { + match chars.next() { + Some('n' | 't' | '"' | '\\') => {} + _ => return Err(LexError::InvalidStringEscape), + } + } + } + Ok(slice) } #[cfg(test)] @@ -204,30 +428,55 @@ mod tests { #[test] fn test_keywords() { assert_eq!(tokenize("contract"), vec![Token::Contract]); + assert_eq!(tokenize("interface"), vec![Token::Interface]); + assert_eq!(tokenize("library"), vec![Token::Library]); assert_eq!(tokenize("import"), vec![Token::Import]); + assert_eq!(tokenize("from"), vec![Token::From]); + assert_eq!(tokenize("export"), vec![Token::Export]); + assert_eq!(tokenize("as"), vec![Token::As]); assert_eq!(tokenize("let"), vec![Token::Let]); - assert_eq!(tokenize("data"), vec![Token::Data]); - assert_eq!(tokenize("class"), vec![Token::Class]); - assert_eq!(tokenize("forall"), vec![Token::Forall]); - assert_eq!(tokenize("instance"), vec![Token::Instance]); + assert_eq!(tokenize("comptime"), vec![Token::Comptime]); + assert_eq!(tokenize("data"), vec![Token::Ident("data")]); + assert_eq!(tokenize("enum"), vec![Token::Enum]); + assert_eq!(tokenize("struct"), vec![Token::Struct]); + assert_eq!(tokenize("trait"), vec![Token::Trait]); + assert_eq!(tokenize("impl"), vec![Token::Impl]); + assert_eq!(tokenize("class"), vec![Token::Ident("class")]); + assert_eq!(tokenize("forall"), vec![Token::Ident("forall")]); + assert_eq!(tokenize("instance"), vec![Token::Ident("instance")]); + assert_eq!(tokenize("alias"), vec![Token::Alias]); + assert_eq!(tokenize("is"), vec![Token::Is]); + assert_eq!(tokenize("where"), vec![Token::Where]); + assert_eq!(tokenize("returns"), vec![Token::Returns]); assert_eq!(tokenize("if"), vec![Token::If]); assert_eq!(tokenize("else"), vec![Token::Else]); assert_eq!(tokenize("for"), vec![Token::For]); + assert_eq!(tokenize("while"), vec![Token::While]); + assert_eq!(tokenize("unchecked"), vec![Token::Unchecked]); assert_eq!(tokenize("switch"), vec![Token::Switch]); assert_eq!(tokenize("type"), vec![Token::Type]); assert_eq!(tokenize("case"), vec![Token::Case]); assert_eq!(tokenize("default"), vec![Token::Default]); assert_eq!(tokenize("match"), vec![Token::Match]); + assert_eq!(tokenize("public"), vec![Token::Public]); + assert_eq!(tokenize("external"), vec![Token::External]); + assert_eq!(tokenize("internal"), vec![Token::Internal]); + assert_eq!(tokenize("private"), vec![Token::Private]); + assert_eq!(tokenize("pure"), vec![Token::Pure]); + assert_eq!(tokenize("view"), vec![Token::View]); + assert_eq!(tokenize("payable"), vec![Token::Payable]); assert_eq!(tokenize("function"), vec![Token::Function]); assert_eq!(tokenize("constructor"), vec![Token::Constructor]); + assert_eq!(tokenize("fallback"), vec![Token::Fallback]); assert_eq!(tokenize("return"), vec![Token::Return]); + assert_eq!(tokenize("revert"), vec![Token::Revert]); assert_eq!(tokenize("leave"), vec![Token::Leave]); assert_eq!(tokenize("continue"), vec![Token::Continue]); assert_eq!(tokenize("break"), vec![Token::Break]); assert_eq!(tokenize("lam"), vec![Token::Lam]); assert_eq!(tokenize("assembly"), vec![Token::Assembly]); assert_eq!(tokenize("pragma"), vec![Token::Pragma]); - assert_eq!(tokenize("then"), vec![Token::Then]); + assert_eq!(tokenize("then"), vec![Token::Ident("then")]); assert_eq!(tokenize("true"), vec![Token::True]); assert_eq!(tokenize("false"), vec![Token::False]); } @@ -245,6 +494,10 @@ mod tests { assert_eq!(tokenize("||"), vec![Token::OrOr]); assert_eq!(tokenize("+="), vec![Token::PlusEq]); assert_eq!(tokenize("-="), vec![Token::MinusEq]); + assert_eq!(tokenize("^="), vec![Token::CaretEq]); + assert_eq!(tokenize("&="), vec![Token::AmpEq]); + assert_eq!(tokenize("|="), vec![Token::PipeEq]); + assert_eq!(tokenize("%="), vec![Token::PercentEq]); } #[test] @@ -255,10 +508,14 @@ mod tests { assert_eq!(tokenize("/"), vec![Token::Slash]); assert_eq!(tokenize("%"), vec![Token::Percent]); assert_eq!(tokenize("!"), vec![Token::Bang]); + assert_eq!(tokenize("~"), vec![Token::Tilde]); assert_eq!(tokenize("<"), vec![Token::Less]); assert_eq!(tokenize(">"), vec![Token::Greater]); assert_eq!(tokenize("="), vec![Token::Eq]); assert_eq!(tokenize("|"), vec![Token::Pipe]); + assert_eq!(tokenize("&"), vec![Token::Amp]); + assert_eq!(tokenize("^"), vec![Token::Caret]); + assert_eq!(tokenize("@"), vec![Token::At]); } #[test] @@ -314,6 +571,8 @@ mod tests { assert_eq!(tokenize("foo_bar"), vec![Token::Ident("foo_bar")]); assert_eq!(tokenize("foo123"), vec![Token::Ident("foo123")]); assert_eq!(tokenize("x1_y2_z3"), vec![Token::Ident("x1_y2_z3")]); + assert_eq!(tokenize("fλ"), vec![Token::Ident("fλ")]); + assert_eq!(tokenize("λ2"), vec![Token::Ident("λ2")]); } #[test] @@ -342,6 +601,13 @@ mod tests { assert_eq!(tokenize("foo_bar"), vec![Token::Ident("foo_bar")]); // Mixed underscores and hyphens. assert_eq!(tokenize("foo_bar-baz"), vec![Token::Ident("foo_bar-baz")]); + assert_eq!(tokenize("comptime"), vec![Token::Comptime]); + } + + #[test] + fn test_invalid_string_escape() { + let mut lexer = Token::lexer(r#""bad\q""#); + assert_eq!(lexer.next(), Some(Err(LexError::InvalidStringEscape))); } #[test] @@ -415,17 +681,23 @@ mod tests { ); assert_eq!( - tokenize("function foo(a, b) -> c"), + tokenize("function foo(a: A, b: B) returns (C)"), vec![ Token::Function, Token::Ident("foo"), Token::LParen, Token::Ident("a"), + Token::Colon, + Token::Ident("A"), Token::Comma, Token::Ident("b"), + Token::Colon, + Token::Ident("B"), + Token::RParen, + Token::Returns, + Token::LParen, + Token::Ident("C"), Token::RParen, - Token::Arrow, - Token::Ident("c"), ] ); } @@ -434,9 +706,9 @@ mod tests { fn test_contract_snippet() { let input = r#" contract Foo { - function bar() -> u256 { - let x := 0x1234; - return x + function bar() returns (u256) { + let x = 0x1234; + return x; } } "#; @@ -452,16 +724,19 @@ mod tests { Token::Ident("bar"), Token::LParen, Token::RParen, - Token::Arrow, + Token::Returns, + Token::LParen, Token::Ident("u256"), + Token::RParen, Token::LBrace, Token::Let, Token::Ident("x"), - Token::ColonEq, + Token::Eq, Token::HexLit("0x1234"), Token::Semi, Token::Return, Token::Ident("x"), + Token::Semi, Token::RBrace, Token::RBrace, ] diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index b235c9ff..c7fbd228 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -1,26 +1,134 @@ -use hir::{Db as HirDb, anchor::DefLocationTable, ast::item, input::SourceFile}; +//! Parser and HIR lowerer for Solcore source files. +//! +//! The parser first produces lightweight parsed syntax with absolute lexical +//! spans, then the lowerer converts it into HIR with stable definition IDs and +//! anchor-relative spans. Parse diagnostics are returned by a pull query, so +//! later HIR visitors can treat `Error` nodes as silent recovery markers. +use hir::{ + Db as HirDb, anchor::DefLocationTable, ast::item, diag::AnyDiagnostic, input::SourceFile, +}; +use logos::Logos; +use tracing::{Level, field}; + +/// Token definitions used by the parser. pub mod lexer; +/// Lowering from parsed syntax into HIR. mod lower; +/// Chumsky grammar and parse entry points. mod parse; +/// Internal parsed-syntax data structures. mod types; +/// Database contract required by parser queries. #[salsa::db] pub trait Db: salsa::Database + HirDb {} +/// Returns whether `text` is one ordinary source identifier. +/// +/// This intentionally follows the lexer and the normal identifier grammar: +/// reserved keywords, pragma-only hyphenated names, `_`, and token sequences +/// are rejected. Editor features such as rename use this to avoid producing +/// source that reparses as a different token kind. +pub fn is_valid_identifier(text: &str) -> bool { + let mut lexer = lexer::Token::lexer(text); + matches!( + lexer.next(), + Some(Ok(lexer::Token::Ident(name))) if name == text && !name.contains('-') + ) && lexer.next().is_none() +} + +/// Output of parsing and lowering one source file. +/// +/// The module contains HIR items and bodies. `def_locations` maps every +/// def-relative anchor emitted during lowering to the absolute byte offset used +/// when diagnostics are eventually rendered. #[salsa::tracked(debug)] pub struct ParseHirOutput<'db> { + /// Lowered module HIR. #[tracked] #[returns(copy)] pub module: item::Module<'db>, + /// Def-anchor base offsets for the source file. #[tracked] #[returns(ref)] pub def_locations: DefLocationTable<'db>, + + /// Parse diagnostics produced while lowering this file. + #[tracked] + #[returns(ref)] + pub diagnostics: Vec, } -/// Parses one source file into HIR in a single pass. +/// Parses one source file into HIR in a single tracked query. +/// +/// The query records def-location data needed to resolve anchor-relative spans +/// at diagnostic/LSP edges. Parse diagnostics are exposed through +/// [`parse_diagnostics`]. #[salsa::tracked] +#[tracing::instrument( + target = "parser::query", + level = "debug", + skip(db, file), + fields(file = field::Empty) +)] pub fn parse_file_to_hir<'db>(db: &'db dyn Db, file: SourceFile) -> ParseHirOutput<'db> { + record_source_file_field(db, file); lower::parse_file_to_hir_impl(db, file) } + +fn record_source_file_field(db: &dyn Db, file: SourceFile) { + if tracing::enabled!(target: "parser::query", Level::DEBUG) { + tracing::Span::current().record("file", field::display(file_url_tail(db, file))); + } +} + +fn file_url_tail(db: &dyn Db, file: SourceFile) -> String { + let url = file.url(db); + if let Some(mut segments) = url.path_segments() + && let Some(last) = segments.next_back() + && !last.is_empty() + { + return last.to_owned(); + } + url.as_str() + .rsplit('/') + .next() + .filter(|tail| !tail.is_empty()) + .unwrap_or(url.as_str()) + .to_owned() +} + +/// Returns parser/lowering diagnostics for one source file. +#[salsa::tracked(returns(ref))] +pub fn parse_diagnostics(db: &dyn Db, file: SourceFile) -> Vec { + parse_file_to_hir(db, file).diagnostics(db).clone() +} + +#[cfg(test)] +mod tests { + use super::is_valid_identifier; + + #[test] + fn validates_identifiers_with_the_source_lexer() { + for valid in ["value", "value_2", "λ", "fλ2"] { + assert!(is_valid_identifier(valid), "expected {valid:?} to be valid"); + } + for invalid in [ + "", + "_", + "_value", + "2value", + "return", + "true", + "value-name", + "two names", + ] { + assert!( + !is_valid_identifier(invalid), + "expected {invalid:?} to be invalid" + ); + } + } +} diff --git a/crates/parser/src/lower.rs b/crates/parser/src/lower.rs deleted file mode 100644 index 0307a60e..00000000 --- a/crates/parser/src/lower.rs +++ /dev/null @@ -1,1224 +0,0 @@ -use hir::{ - anchor::{DefId, DefKind, DefLocation, DefLocationTable, KeyCanonicalizer}, - arena::Arena, - ast::{Ident, function, item, ty}, - diag::{Diagnostic, Offset}, - input::SourceFile, - span::{AnchorId, Span, Spanned, SpannedElem}, -}; - -use crate::{ - Db, ParseHirOutput, - parse::{parse_body_statements, parse_supported_items}, - types::*, -}; - -fn offset_from_usize(raw: usize) -> Offset { - Offset::try_from_usize(raw).expect("span offset exceeds u32::MAX") -} - -fn span_from_absolute<'db>(anchor: AnchorId<'db>, abs: LexSpan, base_start: usize) -> Span<'db> { - let rel_start = abs - .start - .checked_sub(base_start) - .expect("span start is before anchor base"); - let rel_end = abs - .end - .checked_sub(base_start) - .expect("span end is before anchor base"); - Span::new( - anchor, - offset_from_usize(rel_start), - offset_from_usize(rel_end), - ) -} - -fn root_span_from_lex<'db>(db: &'db dyn Db, file: SourceFile, span: LexSpan) -> Span<'db> { - Span::new( - AnchorId::root(db, file), - offset_from_usize(span.start), - offset_from_usize(span.end), - ) -} - -fn accumulate_parse_errors(db: &dyn Db, file: SourceFile, errors: Vec) { - for error in errors { - let _ = Diagnostic::error(error.message) - .with_primary_label(db, root_span_from_lex(db, file, error.span), None::) - .accumulate(db); - } -} - -fn lower_spanned_ident<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - (name, span): SpannedStr<'_>, -) -> SpannedElem<'db, Ident<'db>> { - SpannedElem::new( - Ident::new(db, name.to_owned()), - span_from_absolute(anchor, span, base_start), - ) -} - -fn lower_import<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - path: Vec>, -) -> item::Import<'db> { - let import_def = ctx.alloc_def_with_location(DefKind::Import, None, span.start); - - let anchor = AnchorId::def(ctx.db, import_def); - let path = path - .into_iter() - .map(|segment| lower_spanned_ident(ctx.db, anchor, span.start, segment)) - .collect(); - let span = span_from_absolute(anchor, span, span.start); - item::Import::new(ctx.db, import_def, span, path) -} - -fn lower_pragma<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - name: SpannedStr<'_>, - items: Vec>, -) -> item::Pragma<'db> { - let pragma_def = ctx.alloc_def_with_location(DefKind::Pragma, Some(name.0), span.start); - - let anchor = AnchorId::def(ctx.db, pragma_def); - let name = lower_spanned_ident(ctx.db, anchor, span.start, name); - let items = items - .into_iter() - .map(|segment| lower_spanned_ident(ctx.db, anchor, span.start, segment)) - .collect(); - let span = span_from_absolute(anchor, span, span.start); - item::Pragma::new(ctx.db, pragma_def, span, name, items) -} - -fn lower_type_ref<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - parsed_ty: ParsedTy<'_>, -) -> ty::TypeRef<'db> { - let kind = match parsed_ty.kind { - ParsedTyKind::Named { name, args } => { - let name = lower_spanned_ident(db, anchor, base_start, name); - let args = args - .into_iter() - .map(|arg| lower_type_ref(db, anchor, base_start, arg)) - .collect::>(); - let args_span = span_from_absolute(anchor, parsed_ty.span, base_start); - ty::TypeRefKind::Named { - name, - args: SpannedElem::new(args, args_span), - } - } - ParsedTyKind::Fn { params, ret } => { - let params = params - .into_iter() - .map(|param| lower_type_ref(db, anchor, base_start, param)) - .collect::>(); - let params_span = span_from_absolute(anchor, parsed_ty.span, base_start); - let ret = lower_type_ref(db, anchor, base_start, *ret); - ty::TypeRefKind::Fn { - params: SpannedElem::new(params, params_span), - ret, - } - } - ParsedTyKind::Tuple { elems } => { - let tuple_ty = if elems.len() == 1 { - lower_type_ref( - db, - anchor, - base_start, - elems.into_iter().next().expect("len == 1"), - ) - } else { - ty::TypeRef::new(db, ty::TypeRefKind::Error) - }; - let span = span_from_absolute(anchor, parsed_ty.span, base_start); - ty::TypeRefKind::Tuple { - elems: SpannedElem::new(tuple_ty, span), - } - } - ParsedTyKind::Error => ty::TypeRefKind::Error, - }; - ty::TypeRef::new(db, kind) -} - -fn lower_pred_ref<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - pred: ParsedPred<'_>, -) -> ty::PredRef<'db> { - let ty = lower_type_ref(db, anchor, base_start, pred.ty); - let class = lower_spanned_ident(db, anchor, base_start, pred.class); - let args = pred - .args - .into_iter() - .map(|arg| lower_type_ref(db, anchor, base_start, arg)) - .collect::>(); - let args_span = class.span(db); - ty::PredRef::new( - db, - ty::PredRefKind { - ty, - class, - args: SpannedElem::new(args, args_span), - }, - ) -} - -fn lower_type_alias<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - name: SpannedStr<'_>, - ty_params: Vec>, - parsed_ty: ParsedTy<'_>, -) -> item::TypeAlias<'db> { - let alias_def = ctx.alloc_def_with_location(DefKind::TypeAlias, Some(name.0), span.start); - - let anchor = AnchorId::def(ctx.db, alias_def); - let name = lower_spanned_ident(ctx.db, anchor, span.start, name); - let ty_params = ty_params - .into_iter() - .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) - .collect::>(); - let ty = lower_type_ref(ctx.db, anchor, span.start, parsed_ty); - let span = span_from_absolute(anchor, span, span.start); - item::TypeAlias::new(ctx.db, alias_def, span, name, ty_params, ty) -} - -fn lower_adt_ctor<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - ctor: ParsedAdtCtor<'_>, -) -> item::AdtCtor<'db> { - let name = lower_spanned_ident(db, anchor, base_start, ctor.name); - let fields_ty = if ctor.fields.len() == 1 { - lower_type_ref( - db, - anchor, - base_start, - ctor.fields.into_iter().next().expect("len == 1"), - ) - } else { - ty::TypeRef::new(db, ty::TypeRefKind::Error) - }; - let fields_span = span_from_absolute(anchor, ctor.span, base_start); - item::AdtCtor::new(name, SpannedElem::new(fields_ty, fields_span)) -} - -fn lower_adt<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - name: SpannedStr<'_>, - ty_params: Vec>, - ctors: Vec>, -) -> item::AdtDef<'db> { - let adt_def = ctx.alloc_def_with_location(DefKind::Adt, Some(name.0), span.start); - - let anchor = AnchorId::def(ctx.db, adt_def); - let name = lower_spanned_ident(ctx.db, anchor, span.start, name); - let ty_params = ty_params - .into_iter() - .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) - .collect::>(); - let ctors = ctors - .into_iter() - .map(|ctor| lower_adt_ctor(ctx.db, anchor, span.start, ctor)) - .collect::>(); - let span = span_from_absolute(anchor, span, span.start); - - item::AdtDef::new(ctx.db, adt_def, span, name, ty_params, ctors) -} - -fn lower_func_sig<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - parsed: ParsedFuncSig<'_>, -) -> function::FuncSig<'db> { - let type_vars = parsed - .type_vars - .into_iter() - .map(|it| lower_spanned_ident(db, anchor, base_start, it)) - .collect::>(); - - let preds = parsed - .preds - .into_iter() - .map(|it| lower_pred_ref(db, anchor, base_start, it)) - .collect::>(); - - let name = lower_spanned_ident(db, anchor, base_start, parsed.name); - - let params = parsed - .params - .into_iter() - .map(|param| match param { - ParsedFuncParam::Typed { name, ty } => function::FuncParam::Typed { - name: lower_spanned_ident(db, anchor, base_start, name), - ty: lower_type_ref(db, anchor, base_start, ty), - }, - ParsedFuncParam::Untyped { name } => function::FuncParam::Untyped { - name: lower_spanned_ident(db, anchor, base_start, name), - }, - ParsedFuncParam::Error => function::FuncParam::Error, - }) - .collect::>(); - let params_span = span_from_absolute(anchor, parsed.params_span, base_start); - let params = SpannedElem::new(params, params_span); - - let ret = parsed - .ret - .map(|ret_ty| lower_type_ref(db, anchor, base_start, ret_ty)); - - let span = span_from_absolute(anchor, parsed.span, base_start); - function::FuncSig { - span, - type_vars, - preds, - name, - params, - ret, - } -} - -fn lower_class<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - type_vars: Vec>, - super_preds: Vec>, - head: ParsedPred<'_>, - methods: Vec>, -) -> item::ClassDef<'db> { - let class_name = head.class.0; - let class_def = ctx.alloc_def_with_location(DefKind::Class, Some(class_name), span.start); - - let anchor = AnchorId::def(ctx.db, class_def); - let type_vars = type_vars - .into_iter() - .map(|var| lower_spanned_ident(ctx.db, anchor, span.start, var)) - .collect::>(); - let super_preds = super_preds - .into_iter() - .map(|pred| lower_pred_ref(ctx.db, anchor, span.start, pred)) - .collect::>(); - let head = lower_pred_ref(ctx.db, anchor, span.start, head); - let methods = methods - .into_iter() - .map(|sig| lower_func_sig(ctx.db, anchor, span.start, sig)) - .collect::>(); - let span = span_from_absolute(anchor, span, span.start); - - item::ClassDef::new( - ctx.db, - class_def, - span, - type_vars, - super_preds, - head, - methods, - ) -} - -fn lower_parsed_lit(lit: ParsedLitKind<'_>) -> function::LitKind { - match lit { - ParsedLitKind::Number(n) => function::LitKind::Number(n.to_owned()), - ParsedLitKind::Hex(h) => function::LitKind::Hex(h.to_owned()), - ParsedLitKind::String(s) => function::LitKind::String(s.to_owned()), - } -} - -fn lower_parsed_yul_lit(lit: ParsedYulLitKind<'_>) -> function::YulLitKind { - match lit { - ParsedYulLitKind::Number(n) => function::YulLitKind::Number(n.to_owned()), - ParsedYulLitKind::Hex(h) => function::YulLitKind::Hex(h.to_owned()), - ParsedYulLitKind::String(s) => function::YulLitKind::String(s.to_owned()), - ParsedYulLitKind::Bool(b) => function::YulLitKind::Bool(b), - } -} - -#[derive(Debug)] -struct BodyArenas<'db> { - stmts: Arena>, - exprs: Arena>, - pats: Arena>, -} - -impl<'db> BodyArenas<'db> { - fn new() -> Self { - Self { - stmts: Arena::new(), - exprs: Arena::new(), - pats: Arena::new(), - } - } - - fn into_parts( - self, - ) -> ( - Arena>, - Arena>, - Arena>, - ) { - (self.stmts, self.exprs, self.pats) - } -} - -struct LoweringCtx<'db, 'a> { - db: &'db dyn Db, - file: SourceFile, - keys: &'a mut KeyCanonicalizer, - def_locations: &'a mut Vec<(DefId<'db>, DefLocation)>, - source: &'a str, - parse_errors: &'a mut Vec, -} - -impl<'db, 'a> LoweringCtx<'db, 'a> { - fn new( - db: &'db dyn Db, - file: SourceFile, - keys: &'a mut KeyCanonicalizer, - def_locations: &'a mut Vec<(DefId<'db>, DefLocation)>, - source: &'a str, - parse_errors: &'a mut Vec, - ) -> Self { - Self { - db, - file, - keys, - def_locations, - source, - parse_errors, - } - } - - fn alloc_def_with_location( - &mut self, - kind: DefKind, - name: Option<&str>, - base_start: usize, - ) -> DefId<'db> { - let def = self.keys.alloc_def(self.db, self.file, kind, name); - self.def_locations.push(( - def, - DefLocation { - file: self.file, - base_offset: offset_from_usize(base_start), - }, - )); - def - } - - fn lower_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - expr: ParsedExpr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> hir::arena::Id> { - let span = span_from_absolute(anchor, expr.span, base_start); - let kind = self.lower_expr_kind(anchor, base_start, expr.kind, arenas); - arenas.exprs.alloc(function::Expr { span, kind }) - } - - fn lower_expr_kind( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - kind: ParsedExprKind<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - match kind { - ParsedExprKind::Lit(lit) => function::ExprKind::Lit(lower_parsed_lit(lit)), - ParsedExprKind::Ident(name) => { - function::ExprKind::Ident(lower_spanned_ident(self.db, anchor, base_start, name)) - } - ParsedExprKind::Lambda { - params, - params_span, - ret, - body_span, - } => self.lower_lambda_expr(anchor, base_start, params, params_span, ret, body_span), - ParsedExprKind::BinOp { lhs, op, rhs } => { - self.lower_bin_op_expr(anchor, base_start, *lhs, op, *rhs, arenas) - } - ParsedExprKind::Index { base, index } => { - self.lower_index_expr(anchor, base_start, *base, *index, arenas) - } - ParsedExprKind::Call { callee, args } => { - self.lower_call_expr(anchor, base_start, *callee, args, arenas) - } - ParsedExprKind::Field { base, field } => { - self.lower_field_expr(anchor, base_start, *base, field, arenas) - } - ParsedExprKind::TypeAnnot { expr, ty } => { - self.lower_type_annot_expr(anchor, base_start, *expr, ty, arenas) - } - ParsedExprKind::UnaryOp { op, expr } => { - self.lower_unary_expr(anchor, base_start, op, *expr, arenas) - } - ParsedExprKind::If { - cond, - then_expr, - else_expr, - } => self.lower_if_expr(anchor, base_start, *cond, *then_expr, *else_expr, arenas), - ParsedExprKind::Error => function::ExprKind::Error, - } - } - - fn lower_exprs( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - exprs: Vec>, - arenas: &mut BodyArenas<'db>, - ) -> Vec>> { - exprs - .into_iter() - .map(|expr| self.lower_expr(anchor, base_start, expr, arenas)) - .collect() - } - - fn lower_bin_op_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - lhs: ParsedExpr<'_>, - op: ParsedSpanned<'_, function::BinOp>, - rhs: ParsedExpr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let lhs = self.lower_expr(anchor, base_start, lhs, arenas); - let rhs = self.lower_expr(anchor, base_start, rhs, arenas); - let op_span = span_from_absolute(anchor, op.span, base_start); - function::ExprKind::BinOp { - lhs, - op: SpannedElem::new(op.elem, op_span), - rhs, - } - } - - fn lower_index_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - base: ParsedExpr<'_>, - index: ParsedExpr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let base = self.lower_expr(anchor, base_start, base, arenas); - let index = self.lower_expr(anchor, base_start, index, arenas); - function::ExprKind::Index { base, index } - } - - fn lower_call_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - callee: ParsedExpr<'_>, - args: Vec>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let callee = self.lower_expr(anchor, base_start, callee, arenas); - let args = self.lower_exprs(anchor, base_start, args, arenas); - function::ExprKind::Call { callee, args } - } - - fn lower_field_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - base: ParsedExpr<'_>, - field: SpannedStr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let base = self.lower_expr(anchor, base_start, base, arenas); - let field = lower_spanned_ident(self.db, anchor, base_start, field); - function::ExprKind::Field { base, field } - } - - fn lower_type_annot_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - expr: ParsedExpr<'_>, - ty: ParsedTy<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let expr = self.lower_expr(anchor, base_start, expr, arenas); - let ty = lower_type_ref(self.db, anchor, base_start, ty); - function::ExprKind::TypeAnnot { expr, ty } - } - - fn lower_unary_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - op: ParsedSpanned<'_, function::UnOp>, - expr: ParsedExpr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let expr = self.lower_expr(anchor, base_start, expr, arenas); - let op_span = span_from_absolute(anchor, op.span, base_start); - function::ExprKind::UnaryOp { - op: SpannedElem::new(op.elem, op_span), - expr, - } - } - - fn lower_if_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - cond: ParsedExpr<'_>, - then_expr: ParsedExpr<'_>, - else_expr: ParsedExpr<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::ExprKind<'db> { - let cond = self.lower_expr(anchor, base_start, cond, arenas); - let then_expr = self.lower_expr(anchor, base_start, then_expr, arenas); - let else_expr = self.lower_expr(anchor, base_start, else_expr, arenas); - function::ExprKind::If { - cond, - then_expr, - else_expr, - } - } - - fn lower_lambda_expr( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - params: Vec>, - params_span: LexSpan, - ret: Option>, - body_span: LexSpan, - ) -> function::ExprKind<'db> { - let params = params - .into_iter() - .map(|param| self.lower_func_param(anchor, base_start, param)) - .collect::>(); - let params_span = span_from_absolute(anchor, params_span, base_start); - let params = SpannedElem::new(params, params_span); - let ret = ret.map(|ret_ty| lower_type_ref(self.db, anchor, base_start, ret_ty)); - - let body_def = - self.alloc_def_with_location(DefKind::FuncBody, Some("lambda"), body_span.start); - let body_anchor = AnchorId::def(self.db, body_def); - - let parsed_body = parse_body_statements(self.source, body_span); - self.parse_errors.extend(parsed_body.errors); - - let mut lambda_arenas = BodyArenas::new(); - let mut top_level_stmts = Vec::with_capacity(parsed_body.output.len()); - for stmt in parsed_body.output { - top_level_stmts.push(self.lower_stmt( - body_anchor, - body_span.start, - stmt, - &mut lambda_arenas, - )); - } - - let lowered_body_span = span_from_absolute(body_anchor, body_span, body_span.start); - let (stmts, exprs, pats) = lambda_arenas.into_parts(); - let body = function::FuncBody::new( - self.db, - body_def, - lowered_body_span, - top_level_stmts, - stmts, - exprs, - pats, - ); - - function::ExprKind::Lambda { params, ret, body } - } - - fn lower_func_param( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - param: ParsedFuncParam<'_>, - ) -> function::FuncParam<'db> { - match param { - ParsedFuncParam::Typed { name, ty } => function::FuncParam::Typed { - name: lower_spanned_ident(self.db, anchor, base_start, name), - ty: lower_type_ref(self.db, anchor, base_start, ty), - }, - ParsedFuncParam::Untyped { name } => function::FuncParam::Untyped { - name: lower_spanned_ident(self.db, anchor, base_start, name), - }, - ParsedFuncParam::Error => function::FuncParam::Error, - } - } - - fn lower_stmt( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - stmt: ParsedStmt<'_>, - arenas: &mut BodyArenas<'db>, - ) -> hir::arena::Id> { - let span = span_from_absolute(anchor, stmt.span, base_start); - let kind = self.lower_stmt_kind(anchor, base_start, stmt.kind, arenas); - arenas.stmts.alloc(function::Stmt { span, kind }) - } - - fn lower_stmt_kind( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - kind: ParsedStmtKind<'_>, - arenas: &mut BodyArenas<'db>, - ) -> function::StmtKind<'db> { - match kind { - ParsedStmtKind::Let { name, ty, init } => function::StmtKind::Let { - name: lower_spanned_ident(self.db, anchor, base_start, name), - ty: ty.map(|ty| lower_type_ref(self.db, anchor, base_start, ty)), - init: init.map(|expr| self.lower_expr(anchor, base_start, expr, arenas)), - }, - ParsedStmtKind::Return(expr) => function::StmtKind::Return( - expr.map(|expr| self.lower_expr(anchor, base_start, expr, arenas)), - ), - ParsedStmtKind::Expr(expr) => { - function::StmtKind::Expr(self.lower_expr(anchor, base_start, expr, arenas)) - } - ParsedStmtKind::Assign { lhs, rhs } => function::StmtKind::Assign { - lhs: self.lower_expr(anchor, base_start, lhs, arenas), - rhs: self.lower_expr(anchor, base_start, rhs, arenas), - }, - ParsedStmtKind::AddAssign { lhs, rhs } => function::StmtKind::AddAssign { - lhs: self.lower_expr(anchor, base_start, lhs, arenas), - rhs: self.lower_expr(anchor, base_start, rhs, arenas), - }, - ParsedStmtKind::SubAssign { lhs, rhs } => function::StmtKind::SubAssign { - lhs: self.lower_expr(anchor, base_start, lhs, arenas), - rhs: self.lower_expr(anchor, base_start, rhs, arenas), - }, - ParsedStmtKind::Match { scrutinees, arms } => { - self.lower_match_stmt(anchor, base_start, scrutinees, arms, arenas) - } - ParsedStmtKind::If { - cond, - then_body, - else_body, - } => self.lower_if_stmt(anchor, base_start, cond, then_body, else_body, arenas), - ParsedStmtKind::Assembly { body } => function::StmtKind::Assembly { - body: body - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(self.db, anchor, base_start, stmt)) - .collect(), - }, - ParsedStmtKind::Error => function::StmtKind::Error, - } - } - - fn lower_stmt_block( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - stmts: Vec>, - arenas: &mut BodyArenas<'db>, - ) -> Vec>> { - stmts - .into_iter() - .map(|stmt| self.lower_stmt(anchor, base_start, stmt, arenas)) - .collect() - } - - fn lower_match_stmt( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - scrutinees: Vec>, - arms: Vec>, - arenas: &mut BodyArenas<'db>, - ) -> function::StmtKind<'db> { - let scrutinees = self.lower_exprs(anchor, base_start, scrutinees, arenas); - let arms = arms - .into_iter() - .map(|arm| { - let span = span_from_absolute(anchor, arm.span, base_start); - let pats = arm - .pats - .into_iter() - .map(|pat| lower_parsed_pat(self.db, anchor, base_start, pat, &mut arenas.pats)) - .collect(); - let body = self.lower_stmt_block(anchor, base_start, arm.body, arenas); - function::MatchArm { span, pats, body } - }) - .collect(); - function::StmtKind::Match { scrutinees, arms } - } - - fn lower_if_stmt( - &mut self, - anchor: AnchorId<'db>, - base_start: usize, - cond: ParsedExpr<'_>, - then_body: Vec>, - else_body: Option>>, - arenas: &mut BodyArenas<'db>, - ) -> function::StmtKind<'db> { - let cond = self.lower_expr(anchor, base_start, cond, arenas); - let then_body = self.lower_stmt_block(anchor, base_start, then_body, arenas); - let else_body = - else_body.map(|body| self.lower_stmt_block(anchor, base_start, body, arenas)); - function::StmtKind::If { - cond, - then_body, - else_body, - } - } - - fn lower_body_statements( - &mut self, - anchor: AnchorId<'db>, - body_span: LexSpan, - arenas: &mut BodyArenas<'db>, - ) -> Vec>> { - let parsed = parse_body_statements(self.source, body_span); - self.parse_errors.extend(parsed.errors); - - let mut lowered = Vec::with_capacity(parsed.output.len()); - for stmt in parsed.output { - lowered.push(self.lower_stmt(anchor, body_span.start, stmt, arenas)); - } - lowered - } -} - -fn lower_parsed_pat<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - pat: ParsedPat<'_>, - pats: &mut Arena>, -) -> hir::arena::Id> { - let span = span_from_absolute(anchor, pat.span, base_start); - let kind = match pat.kind { - ParsedPatKind::Wildcard => function::PatKind::Wildcard, - ParsedPatKind::Var(name) => { - function::PatKind::Var(lower_spanned_ident(db, anchor, base_start, name)) - } - ParsedPatKind::Lit(lit) => function::PatKind::Lit(lower_parsed_lit(lit)), - ParsedPatKind::Ctor { name, args } => { - let name = lower_spanned_ident(db, anchor, base_start, name); - let args = args - .into_iter() - .map(|arg| lower_parsed_pat(db, anchor, base_start, arg, pats)) - .collect(); - function::PatKind::Ctor { name, args } - } - ParsedPatKind::Tuple(elems) => { - let elems = elems - .into_iter() - .map(|elem| lower_parsed_pat(db, anchor, base_start, elem, pats)) - .collect(); - function::PatKind::Tuple { elems } - } - ParsedPatKind::Error => function::PatKind::Error, - }; - pats.alloc(function::Pat { span, kind }) -} - -fn lower_parsed_yul_expr<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - expr: ParsedYulExpr<'_>, -) -> function::YulExpr<'db> { - let span = span_from_absolute(anchor, expr.span, base_start); - let kind = match expr.kind { - ParsedYulExprKind::Lit(lit) => function::YulExprKind::Lit(lower_parsed_yul_lit(lit)), - ParsedYulExprKind::Ident(name) => { - function::YulExprKind::Ident(lower_spanned_ident(db, anchor, base_start, name)) - } - ParsedYulExprKind::Call { name, args } => { - let name = lower_spanned_ident(db, anchor, base_start, name); - let args = args - .into_iter() - .map(|arg| lower_parsed_yul_expr(db, anchor, base_start, arg)) - .collect(); - function::YulExprKind::Call { name, args } - } - ParsedYulExprKind::Error => function::YulExprKind::Error, - }; - function::YulExpr { span, kind } -} - -fn lower_parsed_yul_stmt<'db>( - db: &'db dyn Db, - anchor: AnchorId<'db>, - base_start: usize, - stmt: ParsedYulStmt<'_>, -) -> function::YulStmt<'db> { - let span = span_from_absolute(anchor, stmt.span, base_start); - let kind = match stmt.kind { - ParsedYulStmtKind::Block(body) => function::YulStmtKind::Block( - body.into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - ), - ParsedYulStmtKind::Let { names, init } => function::YulStmtKind::Let { - names: names - .into_iter() - .map(|name| lower_spanned_ident(db, anchor, base_start, name)) - .collect(), - init: init.map(|expr| lower_parsed_yul_expr(db, anchor, base_start, expr)), - }, - ParsedYulStmtKind::Assign { names, value } => function::YulStmtKind::Assign { - names: names - .into_iter() - .map(|name| lower_spanned_ident(db, anchor, base_start, name)) - .collect(), - value: lower_parsed_yul_expr(db, anchor, base_start, value), - }, - ParsedYulStmtKind::Expr(expr) => { - function::YulStmtKind::Expr(lower_parsed_yul_expr(db, anchor, base_start, expr)) - } - ParsedYulStmtKind::If { cond, body } => function::YulStmtKind::If { - cond: lower_parsed_yul_expr(db, anchor, base_start, cond), - body: body - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - }, - ParsedYulStmtKind::For { - init, - cond, - post, - body, - } => function::YulStmtKind::For { - init: init - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - cond: lower_parsed_yul_expr(db, anchor, base_start, cond), - post: post - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - body: body - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - }, - ParsedYulStmtKind::Switch { - expr, - cases, - default, - } => function::YulStmtKind::Switch { - expr: lower_parsed_yul_expr(db, anchor, base_start, expr), - cases: cases - .into_iter() - .map(|case| function::YulCase { - span: span_from_absolute(anchor, case.span, base_start), - lit: lower_parsed_yul_lit(case.lit), - body: case - .body - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - }) - .collect(), - default: default.map(|body| { - body.into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect() - }), - }, - ParsedYulStmtKind::FunctionDef { - name, - params, - rets, - body, - } => function::YulStmtKind::FunctionDef { - name: lower_spanned_ident(db, anchor, base_start, name), - params: params - .into_iter() - .map(|param| lower_spanned_ident(db, anchor, base_start, param)) - .collect(), - rets: rets - .into_iter() - .map(|ret| lower_spanned_ident(db, anchor, base_start, ret)) - .collect(), - body: body - .into_iter() - .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) - .collect(), - }, - ParsedYulStmtKind::Leave => function::YulStmtKind::Leave, - ParsedYulStmtKind::Break => function::YulStmtKind::Break, - ParsedYulStmtKind::Continue => function::YulStmtKind::Continue, - ParsedYulStmtKind::Error => function::YulStmtKind::Error, - }; - function::YulStmt { span, kind } -} - -fn lower_function<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - sig: ParsedFuncSig<'_>, - body_span: LexSpan, -) -> item::FunctionDef<'db> { - let func_name = sig.name.0; - let func_def = ctx.alloc_def_with_location(DefKind::Function, Some(func_name), span.start); - - let func_anchor = AnchorId::def(ctx.db, func_def); - let lowered_sig = lower_func_sig(ctx.db, func_anchor, span.start, sig); - let func_span = span_from_absolute(func_anchor, span, span.start); - - let body_def = ctx.alloc_def_with_location(DefKind::FuncBody, Some(func_name), body_span.start); - let body_anchor = AnchorId::def(ctx.db, body_def); - - let mut arenas = BodyArenas::new(); - let top_level_stmts = ctx.lower_body_statements(body_anchor, body_span, &mut arenas); - let lowered_body_span = span_from_absolute(body_anchor, body_span, body_span.start); - let (stmts, exprs, pats) = arenas.into_parts(); - let body = function::FuncBody::new( - ctx.db, - body_def, - lowered_body_span, - top_level_stmts, - stmts, - exprs, - pats, - ); - - item::FunctionDef::new(ctx.db, func_def, func_span, lowered_sig, Some(body)) -} - -fn lower_instance<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - type_vars: Vec>, - preds: Vec>, - default_kw: Option, - head: ParsedPred<'_>, - methods: Vec>, -) -> item::InstanceDef<'db> { - let instance_name = head.class.0; - let instance_def = - ctx.alloc_def_with_location(DefKind::Instance, Some(instance_name), span.start); - - let anchor = AnchorId::def(ctx.db, instance_def); - let type_vars = type_vars - .into_iter() - .map(|var| lower_spanned_ident(ctx.db, anchor, span.start, var)) - .collect::>(); - let preds = preds - .into_iter() - .map(|pred| lower_pred_ref(ctx.db, anchor, span.start, pred)) - .collect::>(); - let default_kw = default_kw.map(|kw_span| span_from_absolute(anchor, kw_span, span.start)); - let head = lower_pred_ref(ctx.db, anchor, span.start, head); - let methods = methods - .into_iter() - .map(|method| lower_function(ctx, method.span, method.sig, method.body_span)) - .collect::>(); - let span = span_from_absolute(anchor, span, span.start); - - item::InstanceDef::new( - ctx.db, - instance_def, - span, - type_vars, - preds, - default_kw, - head, - methods, - ) -} - -fn lower_contract_item<'db>( - ctx: &mut LoweringCtx<'db, '_>, - item: ParsedContractItem<'_>, -) -> item::ContractItem<'db> { - match item { - ParsedContractItem::Function(function) => item::ContractItem::FunctionDef(lower_function( - ctx, - function.span, - function.sig, - function.body_span, - )), - ParsedContractItem::TypeAlias { - span, - name, - ty_params, - ty, - } => item::ContractItem::TypeAlias(lower_type_alias(ctx, span, name, ty_params, ty)), - ParsedContractItem::Adt { - span, - name, - ty_params, - ctors, - } => item::ContractItem::AdtDef(lower_adt(ctx, span, name, ty_params, ctors)), - ParsedContractItem::Error { span } => { - let _ = span; - item::ContractItem::Error - } - } -} - -fn lower_contract<'db>( - ctx: &mut LoweringCtx<'db, '_>, - span: LexSpan, - name: SpannedStr<'_>, - ty_params: Vec>, - fields: Vec>, - items: Vec>, -) -> item::ContractDef<'db> { - let contract_def = ctx.alloc_def_with_location(DefKind::Contract, Some(name.0), span.start); - - let anchor = AnchorId::def(ctx.db, contract_def); - let name = lower_spanned_ident(ctx.db, anchor, span.start, name); - let ty_params = ty_params - .into_iter() - .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) - .collect::>(); - let fields = fields - .into_iter() - .map(|field| { - let _ = field.span; - let name = lower_spanned_ident(ctx.db, anchor, span.start, field.name); - let ty = lower_type_ref(ctx.db, anchor, span.start, field.ty); - item::FieldDef::new(name, ty) - }) - .collect::>(); - let items = items - .into_iter() - .map(|item| lower_contract_item(ctx, item)) - .collect::>(); - let span = span_from_absolute(anchor, span, span.start); - - item::ContractDef::new(ctx.db, contract_def, span, name, ty_params, fields, items) -} - -pub(crate) fn parse_file_to_hir_impl<'db>( - db: &'db dyn Db, - file: SourceFile, -) -> ParseHirOutput<'db> { - let mut keys = KeyCanonicalizer::new(); - let module_def = keys.alloc_def(db, file, DefKind::Module, None); - - let source = file.content(db).as_deref().unwrap_or(""); - let end = offset_from_usize(source.len()); - let module_span = Span::new(AnchorId::root(db, file), Offset::new(0), end); - - let mut items = Vec::new(); - let mut def_locations = vec![( - module_def, - DefLocation { - file, - base_offset: Offset::new(0), - }, - )]; - - let parsed_items = parse_supported_items(source); - let mut parse_errors = parsed_items.errors; - - { - let mut ctx = LoweringCtx::new( - db, - file, - &mut keys, - &mut def_locations, - source, - &mut parse_errors, - ); - - for parsed in parsed_items.output { - match parsed { - ParsedTopItem::Import { span, path } => { - let import = lower_import(&mut ctx, span, path); - items.push(item::Item::Import(import)); - } - ParsedTopItem::Pragma { - span, - name, - items: pragma_items, - } => { - let pragma = lower_pragma(&mut ctx, span, name, pragma_items); - items.push(item::Item::Pragma(pragma)); - } - ParsedTopItem::TypeAlias { - span, - name, - ty_params, - ty, - } => { - let alias = lower_type_alias(&mut ctx, span, name, ty_params, ty); - items.push(item::Item::TypeAlias(alias)); - } - ParsedTopItem::Adt { - span, - name, - ty_params, - ctors, - } => { - let adt = lower_adt(&mut ctx, span, name, ty_params, ctors); - items.push(item::Item::AdtDef(adt)); - } - ParsedTopItem::Class { - span, - type_vars, - super_preds, - head, - methods, - } => { - let class = lower_class(&mut ctx, span, type_vars, super_preds, head, methods); - items.push(item::Item::ClassDef(class)); - } - ParsedTopItem::Instance { - span, - type_vars, - preds, - default_kw, - head, - methods, - } => { - let instance = - lower_instance(&mut ctx, span, type_vars, preds, default_kw, head, methods); - items.push(item::Item::InstanceDef(instance)); - } - ParsedTopItem::Contract { - span, - name, - ty_params, - fields, - items: contract_items, - } => { - let contract = - lower_contract(&mut ctx, span, name, ty_params, fields, contract_items); - items.push(item::Item::ContractDef(contract)); - } - ParsedTopItem::Function { - span, - sig, - body_span, - } => { - let function = lower_function(&mut ctx, span, sig, body_span); - items.push(item::Item::FunctionDef(function)); - } - ParsedTopItem::Error { span } => { - let _ = span; - items.push(item::Item::Error); - } - } - } - } - - let module = item::Module::new(db, module_def, module_span, items); - let def_locations = DefLocationTable::from_def_locations(def_locations); - accumulate_parse_errors(db, file, parse_errors); - - ParseHirOutput::new(db, module, def_locations) -} diff --git a/crates/parser/src/lower/body.rs b/crates/parser/src/lower/body.rs new file mode 100644 index 00000000..8a15324d --- /dev/null +++ b/crates/parser/src/lower/body.rs @@ -0,0 +1,693 @@ +use hir::{ + anchor::DefKind, + arena::Arena, + ast::function, + span::{AnchorId, SpannedElem}, +}; + +use super::{ + context::LoweringCtx, + fingerprint::lambda_fingerprint, + items::lower_type_ref, + span::{lower_qualifier_path, lower_spanned_ident, span_from_absolute}, + yul::lower_parsed_yul_stmt, +}; +use crate::{ + parse::{MAX_EXPRESSION_NESTING, parse_body_statements}, + types::*, +}; + +fn lower_parsed_lit(lit: ParsedLitKind<'_>) -> function::LitKind { + match lit { + ParsedLitKind::Number(n) => function::LitKind::Number(n.to_owned()), + ParsedLitKind::Hex(h) => function::LitKind::Hex(h.to_owned()), + ParsedLitKind::String(s) => function::LitKind::String(s.to_owned()), + } +} + +fn lower_assign_op(op: ParsedAssignOp) -> function::AssignOp { + match op { + ParsedAssignOp::Eq => function::AssignOp::Plain, + ParsedAssignOp::AddEq => function::AssignOp::Add, + ParsedAssignOp::SubEq => function::AssignOp::Sub, + ParsedAssignOp::BitXorEq => function::AssignOp::BitXor, + ParsedAssignOp::BitAndEq => function::AssignOp::BitAnd, + ParsedAssignOp::BitOrEq => function::AssignOp::BitOr, + ParsedAssignOp::ModEq => function::AssignOp::Mod, + } +} + +#[derive(Debug)] +pub(super) struct BodyArenas<'db> { + stmts: Arena>, + exprs: Arena>, + pats: Arena>, +} + +impl<'db> BodyArenas<'db> { + pub(super) fn new() -> Self { + Self { + stmts: Arena::new(), + exprs: Arena::new(), + pats: Arena::new(), + } + } + + pub(super) fn into_parts( + self, + ) -> ( + Arena>, + Arena>, + Arena>, + ) { + (self.stmts, self.exprs, self.pats) + } + + pub(super) fn alloc_stmt( + &mut self, + stmt: function::Stmt<'db>, + ) -> hir::arena::Id> { + self.stmts.alloc(stmt) + } +} + +impl<'db, 'a> LoweringCtx<'db, 'a> { + pub(super) fn lower_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + expr: ParsedExpr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> hir::arena::Id> { + if self.expression_nesting == 0 { + self.expression_nesting_error_reported = false; + } + if self.expression_nesting >= MAX_EXPRESSION_NESTING { + let span = expr.span; + drop_parsed_expr_iteratively(expr); + if !self.expression_nesting_error_reported { + self.parse_errors.push(ParsedError::new( + span, + format!( + "expression nesting exceeds the compiler limit of {MAX_EXPRESSION_NESTING}" + ), + )); + self.expression_nesting_error_reported = true; + } + return arenas.exprs.alloc(function::Expr { + span: span_from_absolute(anchor, span, base_start), + kind: function::ExprKind::Error, + }); + } + + self.expression_nesting += 1; + let span = span_from_absolute(anchor, expr.span, base_start); + let kind = self.lower_expr_kind(anchor, base_start, expr.kind, arenas); + self.expression_nesting -= 1; + arenas.exprs.alloc(function::Expr { span, kind }) + } + + fn lower_expr_kind( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + kind: ParsedExprKind<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + match kind { + ParsedExprKind::Lit(lit) => function::ExprKind::Lit(lower_parsed_lit(lit)), + ParsedExprKind::Ident(name) => { + function::ExprKind::Ident(lower_spanned_ident(self.db, anchor, base_start, name)) + } + ParsedExprKind::Proxy { at, ty } => function::ExprKind::Proxy { + at: span_from_absolute(anchor, at, base_start), + ty: lower_type_ref(self.db, anchor, base_start, ty), + }, + ParsedExprKind::Lambda { + params, + params_span, + ret, + body_span, + } => self.lower_lambda_expr(anchor, base_start, params, params_span, ret, body_span), + ParsedExprKind::BinOp { lhs, op, rhs } => { + self.lower_bin_op_expr(anchor, base_start, *lhs, op, *rhs, arenas) + } + ParsedExprKind::Index { base, index } => { + self.lower_index_expr(anchor, base_start, *base, *index, arenas) + } + ParsedExprKind::Call { callee, args } => { + self.lower_call_expr(anchor, base_start, *callee, args, arenas) + } + ParsedExprKind::Field { base, field } => { + self.lower_field_expr(anchor, base_start, *base, field, arenas) + } + ParsedExprKind::Conversion { expr, ty } => { + self.lower_conversion_expr(anchor, base_start, *expr, ty, arenas) + } + ParsedExprKind::TypeAscription { expr, ty } => { + self.lower_type_ascription_expr(anchor, base_start, *expr, ty, arenas) + } + ParsedExprKind::UnaryOp { op, expr } => { + self.lower_unary_expr(anchor, base_start, op, *expr, arenas) + } + ParsedExprKind::If { + cond, + then_expr, + else_expr, + } => self.lower_if_expr(anchor, base_start, *cond, *then_expr, *else_expr, arenas), + ParsedExprKind::Tuple(elems) => { + self.lower_tuple_expr(anchor, base_start, elems, arenas) + } + ParsedExprKind::Error => function::ExprKind::Error, + } + } + + fn lower_exprs( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + exprs: Vec>, + arenas: &mut BodyArenas<'db>, + ) -> Vec>> { + exprs + .into_iter() + .map(|expr| self.lower_expr(anchor, base_start, expr, arenas)) + .collect() + } + + fn lower_bin_op_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + lhs: ParsedExpr<'_>, + op: ParsedSpanned<'_, function::BinOp>, + rhs: ParsedExpr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let lhs = self.lower_expr(anchor, base_start, lhs, arenas); + let rhs = self.lower_expr(anchor, base_start, rhs, arenas); + let op_span = span_from_absolute(anchor, op.span, base_start); + function::ExprKind::BinOp { + lhs, + op: SpannedElem::new(op.elem, op_span), + rhs, + } + } + + fn lower_index_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + base: ParsedExpr<'_>, + index: ParsedExpr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let base = self.lower_expr(anchor, base_start, base, arenas); + let index = self.lower_expr(anchor, base_start, index, arenas); + function::ExprKind::Index { base, index } + } + + fn lower_call_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + callee: ParsedExpr<'_>, + args: Vec>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let callee = self.lower_expr(anchor, base_start, callee, arenas); + let args = self.lower_exprs(anchor, base_start, args, arenas); + function::ExprKind::Call { callee, args } + } + + fn lower_field_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + base: ParsedExpr<'_>, + field: SpannedStr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let base = self.lower_expr(anchor, base_start, base, arenas); + let field = lower_spanned_ident(self.db, anchor, base_start, field); + function::ExprKind::Field { base, field } + } + + fn lower_conversion_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + expr: ParsedExpr<'_>, + ty: ParsedTy<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let expr = self.lower_expr(anchor, base_start, expr, arenas); + let ty = lower_type_ref(self.db, anchor, base_start, ty); + function::ExprKind::Conversion { expr, ty } + } + + fn lower_type_ascription_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + expr: ParsedExpr<'_>, + ty: ParsedTy<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let expr = self.lower_expr(anchor, base_start, expr, arenas); + let ty = lower_type_ref(self.db, anchor, base_start, ty); + function::ExprKind::TypeAscription { expr, ty } + } + + fn lower_unary_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + op: ParsedSpanned<'_, function::UnOp>, + expr: ParsedExpr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let expr = self.lower_expr(anchor, base_start, expr, arenas); + let op_span = span_from_absolute(anchor, op.span, base_start); + function::ExprKind::UnaryOp { + op: SpannedElem::new(op.elem, op_span), + expr, + } + } + + fn lower_if_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + cond: ParsedExpr<'_>, + then_expr: ParsedExpr<'_>, + else_expr: ParsedExpr<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let cond = self.lower_expr(anchor, base_start, cond, arenas); + let then_expr = self.lower_expr(anchor, base_start, then_expr, arenas); + let else_expr = self.lower_expr(anchor, base_start, else_expr, arenas); + function::ExprKind::If { + cond, + then_expr, + else_expr, + } + } + + fn lower_tuple_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + elems: Vec>, + arenas: &mut BodyArenas<'db>, + ) -> function::ExprKind<'db> { + let elems = self.lower_exprs(anchor, base_start, elems, arenas); + function::ExprKind::Tuple(elems) + } + + fn lower_lambda_expr( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + params: Vec>, + params_span: LexSpan, + ret: Option>, + body_span: LexSpan, + ) -> function::ExprKind<'db> { + let fingerprint = lambda_fingerprint(¶ms, ret.as_ref()); + let params = params + .into_iter() + .map(|param| self.lower_func_param(anchor, base_start, param)) + .collect::>(); + let params_span = span_from_absolute(anchor, params_span, base_start); + let params = SpannedElem::new(params, params_span); + let ret = ret.map(|ret_ty| lower_type_ref(self.db, anchor, base_start, ret_ty)); + + let body_def = self.alloc_def_with_fingerprint( + DefKind::FuncBody, + Some("lambda"), + Some(&fingerprint), + body_span.start, + ); + let body_anchor = AnchorId::def(self.db, body_def); + + let parsed_body = parse_body_statements(self.source, body_span); + self.parse_errors.extend(parsed_body.errors); + + let mut lambda_arenas = BodyArenas::new(); + let mut top_level_stmts = Vec::with_capacity(parsed_body.output.len()); + self.with_owner(body_def, |ctx| { + for stmt in parsed_body.output { + top_level_stmts.push(ctx.lower_stmt( + body_anchor, + body_span.start, + stmt, + &mut lambda_arenas, + )); + } + }); + + let lowered_body_span = span_from_absolute(body_anchor, body_span, body_span.start); + let (stmts, exprs, pats) = lambda_arenas.into_parts(); + let body = function::FuncBody::new( + self.db, + body_def, + lowered_body_span, + top_level_stmts, + stmts, + exprs, + pats, + ); + + function::ExprKind::Lambda { params, ret, body } + } + + fn lower_func_param( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + param: ParsedFuncParam<'_>, + ) -> function::FuncParam<'db> { + match param { + ParsedFuncParam::Typed { comptime, name, ty } => function::FuncParam::Typed { + comptime: comptime.map(|span| span_from_absolute(anchor, span, base_start)), + name: lower_spanned_ident(self.db, anchor, base_start, name), + ty: lower_type_ref(self.db, anchor, base_start, ty), + }, + ParsedFuncParam::Untyped { comptime, name } => function::FuncParam::Untyped { + comptime: comptime.map(|span| span_from_absolute(anchor, span, base_start)), + name: lower_spanned_ident(self.db, anchor, base_start, name), + }, + ParsedFuncParam::Error { span } => function::FuncParam::Error { + span: span_from_absolute(anchor, span, base_start), + }, + } + } + + fn lower_stmt( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + stmt: ParsedStmt<'_>, + arenas: &mut BodyArenas<'db>, + ) -> hir::arena::Id> { + let span = span_from_absolute(anchor, stmt.span, base_start); + let kind = self.lower_stmt_kind(anchor, base_start, stmt.kind, arenas); + arenas.stmts.alloc(function::Stmt { span, kind }) + } + + fn lower_stmt_kind( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + kind: ParsedStmtKind<'_>, + arenas: &mut BodyArenas<'db>, + ) -> function::StmtKind<'db> { + match kind { + ParsedStmtKind::Let { + comptime, + name, + ty, + init, + } => function::StmtKind::Let { + comptime: comptime.map(|span| span_from_absolute(anchor, span, base_start)), + name: lower_spanned_ident(self.db, anchor, base_start, name), + ty: ty.map(|ty| lower_type_ref(self.db, anchor, base_start, ty)), + init: init.map(|expr| self.lower_expr(anchor, base_start, expr, arenas)), + }, + // Every tuple binding is rewritten by `lower_stmt_block`, which + // needs the remainder of the lexical block to preserve the + // binding's scope. + ParsedStmtKind::LetPattern { .. } => function::StmtKind::Error, + ParsedStmtKind::Return(expr) => function::StmtKind::Return( + expr.map(|expr| self.lower_expr(anchor, base_start, expr, arenas)), + ), + ParsedStmtKind::Expr(expr) => { + function::StmtKind::Expr(self.lower_expr(anchor, base_start, expr, arenas)) + } + ParsedStmtKind::Assign { op, lhs, rhs } => function::StmtKind::Assign { + op: lower_assign_op(op), + lhs: self.lower_expr(anchor, base_start, lhs, arenas), + rhs: self.lower_expr(anchor, base_start, rhs, arenas), + }, + ParsedStmtKind::Match { scrutinees, arms } => { + self.lower_match_stmt(anchor, base_start, scrutinees, arms, arenas) + } + ParsedStmtKind::For { + init, + cond, + post, + body, + } => { + let init = self.lower_stmt_block(anchor, base_start, init, arenas); + let cond = self.lower_expr(anchor, base_start, cond, arenas); + let post = self.lower_stmt_block(anchor, base_start, post, arenas); + let body = self.lower_stmt_block(anchor, base_start, body, arenas); + function::StmtKind::For { + init, + cond, + post, + body, + } + } + ParsedStmtKind::If { + cond, + then_body, + else_body, + } => self.lower_if_stmt(anchor, base_start, cond, then_body, else_body, arenas), + ParsedStmtKind::Block { body } => function::StmtKind::Block { + body: self.lower_stmt_block(anchor, base_start, body, arenas), + }, + ParsedStmtKind::Assembly { body } => function::StmtKind::Assembly { + body: body + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(self.db, anchor, base_start, stmt)) + .collect(), + }, + ParsedStmtKind::Break => function::StmtKind::Break, + ParsedStmtKind::Continue => function::StmtKind::Continue, + ParsedStmtKind::Error => function::StmtKind::Error, + } + } + + fn lower_stmt_block( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + stmts: Vec>, + arenas: &mut BodyArenas<'db>, + ) -> Vec>> { + let mut stmts = stmts.into_iter(); + let mut lowered = Vec::new(); + + while let Some(stmt) = stmts.next() { + let stmt_span = stmt.span; + let (pat, ty, init) = match stmt.kind { + ParsedStmtKind::LetPattern { pat, ty, init } => (pat, ty, init), + kind => { + lowered.push(self.lower_stmt( + anchor, + base_start, + ParsedStmt { + span: stmt_span, + kind, + }, + arenas, + )); + continue; + } + }; + + // `let (a, b): (A, B) = value; rest` is equivalent to an + // irrefutable tuple match whose arm contains `rest`. The existing + // match resolver already scopes pattern variables over the arm, + // so this representation keeps the binding visible for every + // following statement in the lexical block. + let scrutinee = match ty { + Some(ty) => ParsedExpr { + span: init.span, + kind: ParsedExprKind::TypeAscription { + expr: Box::new(init), + ty, + }, + }, + None => init, + }; + let tail = stmts.collect::>(); + let end = tail.last().map_or(stmt_span.end, |tail| tail.span.end); + let match_stmt = ParsedStmt { + span: LexSpan::from(stmt_span.start..end), + kind: ParsedStmtKind::Match { + scrutinees: vec![scrutinee], + arms: vec![ParsedMatchArm { + span: LexSpan::from(stmt_span.start..end), + pats: vec![pat], + body: tail, + }], + }, + }; + lowered.push(self.lower_stmt(anchor, base_start, match_stmt, arenas)); + break; + } + + lowered + } + + fn lower_match_stmt( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + scrutinees: Vec>, + arms: Vec>, + arenas: &mut BodyArenas<'db>, + ) -> function::StmtKind<'db> { + let scrutinees = self.lower_exprs(anchor, base_start, scrutinees, arenas); + let mut lowered_arms = Vec::with_capacity(arms.len()); + for arm in arms { + let span = span_from_absolute(anchor, arm.span, base_start); + let pats = arm + .pats + .into_iter() + .map(|pat| lower_parsed_pat(self, anchor, base_start, pat, arenas)) + .collect(); + let body = self.lower_stmt_block(anchor, base_start, arm.body, arenas); + lowered_arms.push(function::MatchArm { span, pats, body }); + } + let arms = lowered_arms; + function::StmtKind::Match { scrutinees, arms } + } + + fn lower_if_stmt( + &mut self, + anchor: AnchorId<'db>, + base_start: usize, + cond: ParsedExpr<'_>, + then_body: Vec>, + else_body: Option>>, + arenas: &mut BodyArenas<'db>, + ) -> function::StmtKind<'db> { + let cond = self.lower_expr(anchor, base_start, cond, arenas); + let then_body = self.lower_stmt_block(anchor, base_start, then_body, arenas); + let else_body = + else_body.map(|body| self.lower_stmt_block(anchor, base_start, body, arenas)); + function::StmtKind::If { + cond, + then_body, + else_body, + } + } + + pub(super) fn lower_body_statements( + &mut self, + anchor: AnchorId<'db>, + body_span: LexSpan, + arenas: &mut BodyArenas<'db>, + ) -> Vec>> { + let parsed = parse_body_statements(self.source, body_span); + self.parse_errors.extend(parsed.errors); + + self.lower_stmt_block(anchor, body_span.start, parsed.output, arenas) + } +} + +fn drop_parsed_expr_iteratively(root: ParsedExpr<'_>) { + let mut pending = vec![root]; + while let Some(expr) = pending.pop() { + match expr.kind { + ParsedExprKind::Tuple(args) => { + pending.extend(args); + } + ParsedExprKind::BinOp { lhs, rhs, .. } => { + pending.push(*lhs); + pending.push(*rhs); + } + ParsedExprKind::Index { base, index } => { + pending.push(*base); + pending.push(*index); + } + ParsedExprKind::Call { callee, args } => { + pending.push(*callee); + pending.extend(args); + } + ParsedExprKind::Field { base, .. } => pending.push(*base), + ParsedExprKind::Conversion { expr, .. } + | ParsedExprKind::TypeAscription { expr, .. } + | ParsedExprKind::UnaryOp { expr, .. } => { + pending.push(*expr); + } + ParsedExprKind::If { + cond, + then_expr, + else_expr, + } => { + pending.push(*cond); + pending.push(*then_expr); + pending.push(*else_expr); + } + ParsedExprKind::Lit(_) + | ParsedExprKind::Ident(_) + | ParsedExprKind::Proxy { .. } + | ParsedExprKind::Lambda { .. } + | ParsedExprKind::Error => {} + } + } +} + +fn lower_parsed_pat<'db>( + ctx: &mut LoweringCtx<'db, '_>, + anchor: AnchorId<'db>, + base_start: usize, + pat: ParsedPat<'_>, + arenas: &mut BodyArenas<'db>, +) -> hir::arena::Id> { + let span = span_from_absolute(anchor, pat.span, base_start); + let kind = match pat.kind { + ParsedPatKind::Wildcard => function::PatKind::Wildcard, + ParsedPatKind::Var(name) => { + function::PatKind::Var(lower_spanned_ident(ctx.db, anchor, base_start, name)) + } + ParsedPatKind::Lit(lit) => function::PatKind::Lit(lower_parsed_lit(lit)), + ParsedPatKind::Ctor { + leading_dot, + qualifiers, + name, + args, + } => { + let leading_dot = leading_dot.map(|dot| span_from_absolute(anchor, dot, base_start)); + let qualifier = lower_qualifier_path(ctx.db, anchor, base_start, qualifiers); + let name = lower_spanned_ident(ctx.db, anchor, base_start, name); + let head = if let Some(dot) = leading_dot { + function::PatCtorHead::Deferred { dot, name } + } else if let Some(qualifier) = qualifier { + function::PatCtorHead::Qualified { qualifier, name } + } else { + function::PatCtorHead::Unqualified { name } + }; + let args = args + .into_iter() + .map(|arg| lower_parsed_pat(ctx, anchor, base_start, arg, arenas)) + .collect(); + function::PatKind::Ctor { head, args } + } + ParsedPatKind::ComptimeLabel { kw, expr } => { + let kw = span_from_absolute(anchor, kw, base_start); + let expr = ctx.lower_expr(anchor, base_start, expr, arenas); + function::PatKind::ComptimeLabel { kw, expr } + } + ParsedPatKind::Tuple(elems) => { + let elems = match <[_; 1]>::try_from(elems) { + Ok([elem]) => { + return lower_parsed_pat(ctx, anchor, base_start, elem, arenas); + } + Err(elems) => elems, + }; + let elems = elems + .into_iter() + .map(|elem| lower_parsed_pat(ctx, anchor, base_start, elem, arenas)) + .collect(); + function::PatKind::Tuple { elems } + } + ParsedPatKind::Error => function::PatKind::Error, + }; + arenas.pats.alloc(function::Pat { span, kind }) +} diff --git a/crates/parser/src/lower/context.rs b/crates/parser/src/lower/context.rs new file mode 100644 index 00000000..f1a8beff --- /dev/null +++ b/crates/parser/src/lower/context.rs @@ -0,0 +1,79 @@ +use hir::{ + anchor::{DefId, DefKind, DefLocation, KeyCanonicalizer}, + input::SourceFile, +}; + +use super::span::offset_from_usize; +use crate::{Db, types::ParsedError}; + +pub(super) struct LoweringCtx<'db, 'a> { + pub(super) db: &'db dyn Db, + pub(super) file: SourceFile, + owner: Option>, + keys: &'a mut KeyCanonicalizer, + def_locations: &'a mut Vec<(DefId<'db>, DefLocation)>, + pub(super) source: &'a str, + pub(super) parse_errors: &'a mut Vec, + pub(super) expression_nesting: usize, + pub(super) expression_nesting_error_reported: bool, +} + +impl<'db, 'a> LoweringCtx<'db, 'a> { + pub(super) fn new( + db: &'db dyn Db, + file: SourceFile, + owner: Option>, + keys: &'a mut KeyCanonicalizer, + def_locations: &'a mut Vec<(DefId<'db>, DefLocation)>, + source: &'a str, + parse_errors: &'a mut Vec, + ) -> Self { + Self { + db, + file, + owner, + keys, + def_locations, + source, + parse_errors, + expression_nesting: 0, + expression_nesting_error_reported: false, + } + } + + pub(super) fn with_owner(&mut self, owner: DefId<'db>, f: impl FnOnce(&mut Self) -> T) -> T { + let previous = self.owner.replace(owner); + let result = f(self); + self.owner = previous; + result + } + + pub(super) fn alloc_def_with_location( + &mut self, + kind: DefKind, + name: Option<&str>, + base_start: usize, + ) -> DefId<'db> { + self.alloc_def_with_fingerprint(kind, name, None, base_start) + } + + pub(super) fn alloc_def_with_fingerprint( + &mut self, + kind: DefKind, + name: Option<&str>, + fingerprint: Option<&str>, + base_start: usize, + ) -> DefId<'db> { + let def = self + .keys + .alloc_def(self.db, self.file, self.owner, kind, name, fingerprint); + self.def_locations.push(( + def, + DefLocation { + file: self.file, + base_offset: offset_from_usize(base_start), + }, + )); + def + } +} diff --git a/crates/parser/src/lower/fingerprint.rs b/crates/parser/src/lower/fingerprint.rs new file mode 100644 index 00000000..08172384 --- /dev/null +++ b/crates/parser/src/lower/fingerprint.rs @@ -0,0 +1,280 @@ +use super::span::path_text; +use crate::types::*; + +pub(super) fn import_fingerprint( + external: Option, + path: &[SpannedStr<'_>], + alias: Option<&SpannedStr<'_>>, + selector: Option<&ParsedImportSelector<'_>>, + hiding: &[ParsedImportName], +) -> String { + // Import identity is based on normalized import semantics, not the byte + // location of the declaration. Selector and hiding lists are sorted so + // reordering names does not churn the DefId. + let mut fingerprint = if external.is_some() { + "@".to_owned() + } else { + String::new() + }; + fingerprint.push_str( + &path + .iter() + .map(|(name, _)| *name) + .collect::>() + .join("."), + ); + + if let Some((alias, _)) = alias { + fingerprint.push_str(" as "); + fingerprint.push_str(alias); + } + + if let Some(selector) = selector { + match selector { + ParsedImportSelector::Wildcard => fingerprint.push_str(".*"), + ParsedImportSelector::Names(names) => { + fingerprint.push_str(".{"); + fingerprint.push_str(&sorted_fingerprints(names, selected_fingerprint)); + fingerprint.push('}'); + } + } + } + + if !hiding.is_empty() { + fingerprint.push_str(" hiding {"); + fingerprint.push_str(&sorted_fingerprints(hiding, import_name_fingerprint)); + fingerprint.push('}'); + } + + fingerprint +} + +fn selected_fingerprint(name: &ParsedSelectedName<'_>) -> String { + let mut fingerprint = import_name_fingerprint(&name.name); + if let Some(constructors) = &name.constructors { + fingerprint.push_str(&constructor_selector_fingerprint(constructors)); + } + if let Some((alias, _)) = &name.alias { + fingerprint.push_str(" as "); + fingerprint.push_str(alias); + } + fingerprint +} + +fn constructor_selector_fingerprint(selector: &ParsedConstructorSelector<'_>) -> String { + match selector { + ParsedConstructorSelector::All => "(*)".to_owned(), + ParsedConstructorSelector::Named(names) => { + let mut names = names.iter().map(|(name, _)| *name).collect::>(); + names.sort_unstable(); + format!("({})", names.join(",")) + } + } +} + +fn import_name_fingerprint(name: &ParsedImportName) -> String { + let kind = if name.is_operator { "op" } else { "name" }; + format!("{kind}:{}", name.name) +} + +pub(super) fn export_fingerprint(kind: &ParsedExportKind<'_>) -> String { + match kind { + ParsedExportKind::List(names) => { + format!( + "list{{{}}}", + sorted_fingerprints(names, export_name_fingerprint) + ) + } + ParsedExportKind::Module(path) => format!("module {}", path_fingerprint(path)), + ParsedExportKind::ModuleAs(path, alias) => { + format!("module {} as {}", path_fingerprint(path), alias.0) + } + ParsedExportKind::ItemsFrom(path, names) => { + format!( + "items {}.{{{}}}", + path_fingerprint(path), + sorted_fingerprints(names, export_name_fingerprint) + ) + } + } +} + +fn export_name_fingerprint(name: &ParsedExportName<'_>) -> String { + let mut fingerprint = import_name_fingerprint(&name.name); + if let Some(constructors) = &name.constructors { + fingerprint.push_str(&constructor_selector_fingerprint(constructors)); + } + fingerprint +} + +fn path_fingerprint(path: &[SpannedStr<'_>]) -> String { + path.iter() + .map(|(name, _)| *name) + .collect::>() + .join(".") +} + +fn sorted_fingerprints(items: &[T], fingerprint: fn(&T) -> String) -> String { + let mut fingerprints = items.iter().map(fingerprint).collect::>(); + fingerprints.sort_unstable(); + fingerprints.join(",") +} + +pub(super) fn lambda_fingerprint( + params: &[ParsedFuncParam<'_>], + ret: Option<&ParsedTy<'_>>, +) -> String { + let mut components = Vec::with_capacity(params.len() + 1); + for param in params { + components.push(lambda_param_fingerprint(param)); + } + components.push(optional_ty_fingerprint(ret)); + structural_fingerprint("lambda", &components) +} + +fn lambda_param_fingerprint(param: &ParsedFuncParam<'_>) -> String { + match param { + ParsedFuncParam::Typed { comptime, name, ty } => structural_fingerprint( + "param", + &[ + "typed".to_owned(), + comptime.is_some().to_string(), + name.0.to_owned(), + ty_fingerprint_or_error(ty), + ], + ), + ParsedFuncParam::Untyped { comptime, name } => structural_fingerprint( + "param", + &[ + "untyped".to_owned(), + comptime.is_some().to_string(), + name.0.to_owned(), + ], + ), + ParsedFuncParam::Error { .. } => structural_fingerprint("param", &["error".to_owned()]), + } +} + +fn optional_ty_fingerprint(ty: Option<&ParsedTy<'_>>) -> String { + ty.map(ty_fingerprint_or_error) + .unwrap_or_else(|| "".to_owned()) +} + +fn ty_fingerprint_or_error(ty: &ParsedTy<'_>) -> String { + canonical_ty_fingerprint(ty, &[]).unwrap_or_else(|| "".to_owned()) +} + +pub(super) fn instance_head_fingerprint( + type_vars: &[SpannedStr<'_>], + head: &ParsedPred<'_>, +) -> Option { + let type_vars = type_vars + .iter() + .enumerate() + .map(|(index, (name, _))| (*name, index)) + .collect::>(); + + let mut components = Vec::with_capacity(1 + head.args.len()); + components.push(canonical_ty_fingerprint(&head.ty, &type_vars)?); + for arg in &head.args { + components.push(canonical_ty_fingerprint(arg, &type_vars)?); + } + Some(structural_fingerprint("pred", &components)) +} + +fn structural_fingerprint(label: &str, components: &[String]) -> String { + // Length prefixes make the encoding unambiguous even when component strings + // contain punctuation used by the fingerprint syntax. + let mut fingerprint = format!("{label}[{}]", components.len()); + for component in components { + fingerprint.push('|'); + fingerprint.push_str(&component.len().to_string()); + fingerprint.push(':'); + fingerprint.push_str(component); + } + fingerprint +} + +fn canonical_ty_fingerprint(ty: &ParsedTy<'_>, type_vars: &[(&str, usize)]) -> Option { + match &ty.kind { + ParsedTyKind::Named { + qualifiers, + name, + args, + args_span: _, + } => { + let name = if args.is_empty() && qualifiers.is_empty() { + // Instance identity is alpha-equivalent over its declared type + // variables, so binders are encoded by position rather than by + // surface spelling. + type_vars + .iter() + .find_map(|(var, index)| (*var == name.0).then_some(format!("${index}"))) + .unwrap_or_else(|| name.0.to_owned()) + } else if qualifiers.is_empty() { + name.0.to_owned() + } else { + format!("{}.{}", path_text(qualifiers), name.0) + }; + + if args.is_empty() { + Some(name) + } else { + let args = args + .iter() + .map(|arg| canonical_ty_fingerprint(arg, type_vars)) + .collect::>>()?; + Some(format!("{name}({})", args.join(","))) + } + } + ParsedTyKind::FixedArray { + element, length, .. + } => canonical_ty_fingerprint(element, type_vars) + .map(|element| format!("fixed-array({length};{element})")), + ParsedTyKind::Proxy { inner, .. } => { + canonical_ty_fingerprint(inner, type_vars).map(|inner| format!("Proxy({inner})")) + } + ParsedTyKind::Fn { + params, + params_span: _, + visibility, + mutability, + ret, + } => { + let params = params + .iter() + .map(|param| canonical_ty_fingerprint(param, type_vars)) + .collect::>>()?; + let ret = canonical_ty_fingerprint(ret, type_vars)?; + let qualifiers = [ + visibility + .as_ref() + .map(|(visibility, _)| visibility.keyword()), + mutability + .as_ref() + .map(|(mutability, _)| mutability.keyword()), + ] + .into_iter() + .flatten() + .collect::>() + .join(" "); + let head = if qualifiers.is_empty() { + "fn".to_owned() + } else { + format!("fn[{qualifiers}]") + }; + Some(format!("{head}({})->{ret}", params.join(","))) + } + ParsedTyKind::Comptime { inner, .. } => { + canonical_ty_fingerprint(inner, type_vars).map(|inner| format!("comptime({inner})")) + } + ParsedTyKind::Tuple { elems } => { + let elems = elems + .iter() + .map(|elem| canonical_ty_fingerprint(elem, type_vars)) + .collect::>>()?; + Some(format!("({})", elems.join(","))) + } + ParsedTyKind::Error => None, + } +} diff --git a/crates/parser/src/lower/items.rs b/crates/parser/src/lower/items.rs new file mode 100644 index 00000000..8dbd7282 --- /dev/null +++ b/crates/parser/src/lower/items.rs @@ -0,0 +1,873 @@ +use hir::{ + anchor::DefKind, + ast::{Ident, function, item, ty}, + diag::{AnyDiagnostic, Diagnostic}, + input::SourceFile, + span::{AnchorId, Spanned, SpannedElem}, +}; + +use super::{ + body::BodyArenas, + context::LoweringCtx, + fingerprint::{export_fingerprint, import_fingerprint, instance_head_fingerprint}, + span::{ + lower_owned_ident, lower_path, lower_qualifier_path, lower_spanned_ident, + root_span_from_lex, span_from_absolute, + }, +}; +use crate::{Db, types::*}; + +pub(super) fn lower_parse_errors( + db: &dyn Db, + file: SourceFile, + errors: Vec, +) -> Vec { + errors + .into_iter() + .map(|error| { + let mut diagnostic = Diagnostic::error(error.message) + .with_code("SC0001") + .with_primary_label(db, root_span_from_lex(db, file, error.span), error.label); + for note in error.notes { + diagnostic = diagnostic.with_note(note); + } + AnyDiagnostic::Parse(diagnostic) + }) + .collect() +} + +pub(super) fn lower_import<'db>( + ctx: &mut LoweringCtx<'db, '_>, + meta: ParsedItemMeta<'_>, + external: Option, + path: Vec>, + alias: Option>, + selector: Option>, + hiding: Vec, +) -> item::Import<'db> { + let ParsedItemMeta { + span, + leading_comments, + } = meta; + let fingerprint = + import_fingerprint(external, &path, alias.as_ref(), selector.as_ref(), &hiding); + let import_def = + ctx.alloc_def_with_fingerprint(DefKind::Import, None, Some(&fingerprint), span.start); + + let anchor = AnchorId::def(ctx.db, import_def); + let base_start = span.start; + let external = external.map(|span| span_from_absolute(anchor, span, base_start)); + let path = lower_path(ctx.db, anchor, base_start, path); + let alias = alias.map(|it| lower_spanned_ident(ctx.db, anchor, base_start, it)); + let selector = + selector.map(|selector| lower_import_selector(ctx.db, anchor, base_start, selector)); + let hiding = hiding + .into_iter() + .map(|it| item::ImportHiddenName { + name: lower_owned_ident(ctx.db, anchor, base_start, it.name, it.span), + is_operator: it.is_operator, + }) + .collect(); + let span = span_from_absolute(anchor, span, base_start); + item::Import::new( + ctx.db, + import_def, + span, + lower_source_comments(leading_comments), + external, + path, + alias, + selector, + hiding, + ) +} + +fn lower_import_selector<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + selector: ParsedImportSelector<'_>, +) -> item::ImportSelector<'db> { + match selector { + ParsedImportSelector::Wildcard => item::ImportSelector::Wildcard, + ParsedImportSelector::Names(names) => item::ImportSelector::Names( + names + .into_iter() + .map(|it| item::SelectedName { + name: lower_owned_ident(db, anchor, base_start, it.name.name, it.name.span), + alias: it + .alias + .map(|alias| lower_spanned_ident(db, anchor, base_start, alias)), + constructors: it.constructors.map(|constructors| { + lower_constructor_selector(db, anchor, base_start, constructors) + }), + is_operator: it.name.is_operator, + }) + .collect(), + ), + } +} + +fn lower_constructor_selector<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + selector: ParsedConstructorSelector<'_>, +) -> item::ConstructorSelector<'db> { + match selector { + ParsedConstructorSelector::All => item::ConstructorSelector::All, + ParsedConstructorSelector::Named(names) => item::ConstructorSelector::Named( + names + .into_iter() + .map(|name| lower_spanned_ident(db, anchor, base_start, name)) + .collect(), + ), + } +} + +pub(super) fn lower_export<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + kind: ParsedExportKind<'_>, +) -> item::Export<'db> { + let fingerprint = export_fingerprint(&kind); + let export_def = + ctx.alloc_def_with_fingerprint(DefKind::Export, None, Some(&fingerprint), span.start); + + let anchor = AnchorId::def(ctx.db, export_def); + let base_start = span.start; + let kind = lower_export_kind(ctx.db, anchor, base_start, kind); + let span = span_from_absolute(anchor, span, base_start); + item::Export::new( + ctx.db, + export_def, + span, + lower_source_comments(leading_comments), + kind, + ) +} + +fn lower_export_kind<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + kind: ParsedExportKind<'_>, +) -> item::ExportKind<'db> { + match kind { + ParsedExportKind::List(names) => { + item::ExportKind::List(lower_exported_names(db, anchor, base_start, names)) + } + ParsedExportKind::Module(path) => { + item::ExportKind::Module(lower_path(db, anchor, base_start, path)) + } + ParsedExportKind::ModuleAs(path, alias) => item::ExportKind::ModuleAs( + lower_path(db, anchor, base_start, path), + lower_spanned_ident(db, anchor, base_start, alias), + ), + ParsedExportKind::ItemsFrom(path, names) => item::ExportKind::ItemsFrom( + lower_path(db, anchor, base_start, path), + lower_exported_names(db, anchor, base_start, names), + ), + } +} + +fn lower_exported_names<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + names: Vec>, +) -> Vec> { + names + .into_iter() + .map(|name| lower_exported_name(db, anchor, base_start, name)) + .collect() +} + +fn lower_exported_name<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + name: ParsedExportName<'_>, +) -> item::ExportedName<'db> { + item::ExportedName { + name: lower_owned_ident(db, anchor, base_start, name.name.name, name.name.span), + constructors: name + .constructors + .map(|constructors| lower_constructor_selector(db, anchor, base_start, constructors)), + is_operator: name.name.is_operator, + } +} + +pub(super) fn lower_pragma<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + name: SpannedStr<'_>, + items: Vec>, +) -> item::Pragma<'db> { + let pragma_def = ctx.alloc_def_with_location(DefKind::Pragma, Some(name.0), span.start); + + let anchor = AnchorId::def(ctx.db, pragma_def); + let name = lower_spanned_ident(ctx.db, anchor, span.start, name); + let items = items + .into_iter() + .map(|segment| lower_spanned_ident(ctx.db, anchor, span.start, segment)) + .collect(); + let span = span_from_absolute(anchor, span, span.start); + item::Pragma::new( + ctx.db, + pragma_def, + span, + lower_source_comments(leading_comments), + name, + items, + ) +} + +pub(super) fn lower_type_ref<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + parsed_ty: ParsedTy<'_>, +) -> ty::TypeRef<'db> { + let ty_span = parsed_ty.span; + let kind = match parsed_ty.kind { + ParsedTyKind::Named { + qualifiers, + name, + args, + args_span, + } => { + let qualifier = lower_qualifier_path(db, anchor, base_start, qualifiers); + let args_span = args_span.unwrap_or_else(|| LexSpan::from(name.1.end..name.1.end)); + let name = lower_spanned_ident(db, anchor, base_start, name); + let args = args + .into_iter() + .map(|arg| lower_type_ref(db, anchor, base_start, arg)) + .collect::>(); + let args_span = span_from_absolute(anchor, args_span, base_start); + ty::TypeRefKind::Named { + qualifier, + name, + args: SpannedElem::new(args, args_span), + } + } + ParsedTyKind::FixedArray { + element, + length, + brackets_span, + } => ty::TypeRefKind::FixedArray { + element: lower_type_ref(db, anchor, base_start, *element), + length, + brackets: span_from_absolute(anchor, brackets_span, base_start), + }, + ParsedTyKind::Proxy { at, inner } => { + let inner = lower_type_ref(db, anchor, base_start, *inner); + ty::TypeRefKind::Named { + qualifier: None, + name: SpannedElem::new( + Ident::new(db, "Proxy".to_owned()), + span_from_absolute(anchor, at, base_start), + ), + args: SpannedElem::new( + vec![inner], + span_from_absolute(anchor, ty_span, base_start), + ), + } + } + ParsedTyKind::Fn { + params, + params_span, + visibility, + mutability, + ret, + } => { + let params = params + .into_iter() + .map(|param| lower_type_ref(db, anchor, base_start, param)) + .collect::>(); + let params_span = span_from_absolute(anchor, params_span, base_start); + let visibility = visibility.map(|(visibility, span)| { + SpannedElem::new(visibility, span_from_absolute(anchor, span, base_start)) + }); + let mutability = mutability.map(|(mutability, span)| { + SpannedElem::new(mutability, span_from_absolute(anchor, span, base_start)) + }); + let ret = lower_type_ref(db, anchor, base_start, *ret); + ty::TypeRefKind::Fn { + span: span_from_absolute(anchor, ty_span, base_start), + params: SpannedElem::new(params, params_span), + visibility, + mutability, + ret, + } + } + ParsedTyKind::Comptime { kw, inner } => ty::TypeRefKind::Comptime { + kw: span_from_absolute(anchor, kw, base_start), + inner: lower_type_ref(db, anchor, base_start, *inner), + }, + ParsedTyKind::Tuple { elems } => { + return lower_type_list_ref(db, anchor, base_start, ty_span, elems); + } + ParsedTyKind::Error => ty::TypeRefKind::Error { + span: span_from_absolute(anchor, ty_span, base_start), + }, + }; + ty::TypeRef::new(db, kind) +} + +fn lower_type_list_ref<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + span: LexSpan, + elems: Vec>, +) -> ty::TypeRef<'db> { + let elems = match <[_; 1]>::try_from(elems) { + Ok([elem]) => return lower_type_ref(db, anchor, base_start, elem), + Err(elems) => elems, + }; + + let span = span_from_absolute(anchor, span, base_start); + let elems = elems + .into_iter() + .map(|elem| lower_type_ref(db, anchor, base_start, elem)) + .collect::>(); + ty::TypeRef::new( + db, + ty::TypeRefKind::Tuple { + elems: SpannedElem::new(elems, span), + }, + ) +} + +fn lower_pred_ref<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + pred: ParsedPred<'_>, +) -> ty::PredRef<'db> { + let ty = lower_type_ref(db, anchor, base_start, pred.ty); + let args_span = pred + .args_span + .unwrap_or_else(|| LexSpan::from(pred.class.1.end..pred.class.1.end)); + let class = lower_spanned_ident(db, anchor, base_start, pred.class); + let args = pred + .args + .into_iter() + .map(|arg| lower_type_ref(db, anchor, base_start, arg)) + .collect::>(); + let args_span = span_from_absolute(anchor, args_span, base_start); + ty::PredRef::new( + db, + ty::PredRefKind { + ty, + class, + args: SpannedElem::new(args, args_span), + }, + ) +} + +pub(super) fn lower_type_alias<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + kind: ParsedTypeAliasKind, + name: SpannedStr<'_>, + ty_params: Vec>, + parsed_ty: ParsedTy<'_>, +) -> item::TypeAlias<'db> { + let hir_kind = match kind { + ParsedTypeAliasKind::Transparent => item::TypeAliasKind::Transparent, + ParsedTypeAliasKind::ValueType => item::TypeAliasKind::ValueType, + }; + let def_kind = match hir_kind { + item::TypeAliasKind::Transparent => DefKind::TypeAlias, + item::TypeAliasKind::ValueType => DefKind::ValueType, + }; + let alias_def = ctx.alloc_def_with_location(def_kind, Some(name.0), span.start); + + let anchor = AnchorId::def(ctx.db, alias_def); + let name = lower_spanned_ident(ctx.db, anchor, span.start, name); + let ty_params = ty_params + .into_iter() + .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) + .collect::>(); + let ty = lower_type_ref(ctx.db, anchor, span.start, parsed_ty); + let span = span_from_absolute(anchor, span, span.start); + item::TypeAlias::new( + ctx.db, + alias_def, + span, + lower_source_comments(leading_comments), + hir_kind, + name, + ty_params, + ty, + ) +} + +fn lower_adt_ctor<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + ctor: ParsedAdtCtor<'_>, +) -> item::AdtCtor<'db> { + let field_count = ctor.fields.len(); + let name = lower_spanned_ident(db, anchor, base_start, ctor.name); + let field_names = ctor.field_names.map(|names| { + names + .into_iter() + .map(|name| lower_spanned_ident(db, anchor, base_start, name)) + .collect() + }); + let fields_span = span_from_absolute(anchor, ctor.span, base_start); + let fields_ty = lower_type_list_ref(db, anchor, base_start, ctor.span, ctor.fields); + item::AdtCtor::new( + name, + SpannedElem::new(fields_ty, fields_span), + field_names, + field_count, + ) +} + +pub(super) fn lower_adt<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + name: SpannedStr<'_>, + ty_params: Vec>, + ctors: Vec>, +) -> item::AdtDef<'db> { + let adt_def = ctx.alloc_def_with_location(DefKind::Adt, Some(name.0), span.start); + + let anchor = AnchorId::def(ctx.db, adt_def); + let name = lower_spanned_ident(ctx.db, anchor, span.start, name); + let ty_params = ty_params + .into_iter() + .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) + .collect::>(); + let (ctors, ctor_comments) = ctors + .into_iter() + .map(|mut ctor| { + let comments = lower_source_comments(std::mem::take(&mut ctor.leading_comments)); + let ctor = lower_adt_ctor(ctx.db, anchor, span.start, ctor); + (ctor, comments) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + let span = span_from_absolute(anchor, span, span.start); + + item::AdtDef::new( + ctx.db, + adt_def, + span, + lower_source_comments(leading_comments), + name, + ty_params, + ctors, + ctor_comments, + ) +} + +fn lower_func_sig<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + parsed: ParsedFuncSig<'_>, +) -> function::FuncSig<'db> { + let type_vars = parsed + .type_vars + .into_iter() + .map(|it| lower_spanned_ident(db, anchor, base_start, it)) + .collect::>(); + + let preds = parsed + .preds + .into_iter() + .map(|it| lower_pred_ref(db, anchor, base_start, it)) + .collect::>(); + + let name = lower_spanned_ident(db, anchor, base_start, parsed.name); + + let params = parsed + .params + .into_iter() + .map(|param| match param { + ParsedFuncParam::Typed { comptime, name, ty } => function::FuncParam::Typed { + comptime: comptime.map(|span| span_from_absolute(anchor, span, base_start)), + name: lower_spanned_ident(db, anchor, base_start, name), + ty: lower_type_ref(db, anchor, base_start, ty), + }, + ParsedFuncParam::Untyped { comptime, name } => function::FuncParam::Untyped { + comptime: comptime.map(|span| span_from_absolute(anchor, span, base_start)), + name: lower_spanned_ident(db, anchor, base_start, name), + }, + ParsedFuncParam::Error { span } => function::FuncParam::Error { + span: span_from_absolute(anchor, span, base_start), + }, + }) + .collect::>(); + let params_span = span_from_absolute(anchor, parsed.params_span, base_start); + let params = SpannedElem::new(params, params_span); + + let ret = parsed + .ret + .map(|ret_ty| lower_type_ref(db, anchor, base_start, ret_ty)); + let ret_names = parsed + .ret_names + .into_iter() + .map(|name| name.map(|name| lower_spanned_ident(db, anchor, base_start, name))) + .collect(); + + let span = span_from_absolute(anchor, parsed.span, base_start); + let visibility = parsed.visibility.map(|(visibility, span)| { + SpannedElem::new(visibility, span_from_absolute(anchor, span, base_start)) + }); + let mutability = parsed.mutability.map(|(mutability, span)| { + SpannedElem::new(mutability, span_from_absolute(anchor, span, base_start)) + }); + function::FuncSig { + span, + type_vars, + preds, + visibility, + mutability, + name, + params, + ret, + ret_names, + } +} + +fn named_return_bindings<'db>( + db: &'db dyn Db, + sig: &function::FuncSig<'db>, +) -> Vec<(SpannedElem<'db, Ident<'db>>, ty::TypeRef<'db>)> { + let result_tys = match (sig.ret, sig.ret_names.len()) { + (Some(ret), 1) => vec![ret], + (Some(ret), count) if count > 1 => match ret.kind(db) { + ty::TypeRefKind::Tuple { elems } if elems.atom().len() == count => elems.atom().clone(), + _ => vec![ret], + }, + _ => Vec::new(), + }; + sig.ret_names + .iter() + .zip(result_tys) + .filter_map(|(name, ty)| name.map(|name| (name, ty))) + .collect() +} + +pub(super) fn lower_class<'db, 'src>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + type_vars: Vec>, + super_preds: Vec>, + head: ParsedPred<'src>, + methods: Vec>, +) -> item::ClassDef<'db> { + let class_name = head.class.0; + let class_def = ctx.alloc_def_with_location(DefKind::Class, Some(class_name), span.start); + + let anchor = AnchorId::def(ctx.db, class_def); + let type_vars = type_vars + .into_iter() + .map(|var| lower_spanned_ident(ctx.db, anchor, span.start, var)) + .collect::>(); + let super_preds = super_preds + .into_iter() + .map(|pred| lower_pred_ref(ctx.db, anchor, span.start, pred)) + .collect::>(); + let head = lower_pred_ref(ctx.db, anchor, span.start, head); + let (methods, method_comments) = methods + .into_iter() + .map(|method| { + ( + lower_func_sig(ctx.db, anchor, span.start, method.sig), + lower_source_comments(method.leading_comments), + ) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + let span = span_from_absolute(anchor, span, span.start); + + item::ClassDef::new( + ctx.db, + class_def, + span, + lower_source_comments(leading_comments), + type_vars, + super_preds, + head, + methods, + method_comments, + ) +} + +pub(super) fn lower_source_comments( + comments: Vec>, +) -> Vec { + comments + .into_iter() + .map(|comment| item::SourceComment { + kind: match comment.kind { + ParsedSourceCommentKind::Line => item::SourceCommentKind::Line, + ParsedSourceCommentKind::Block => item::SourceCommentKind::Block, + }, + text: comment.text.to_owned(), + }) + .collect() +} + +pub(super) fn lower_function<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + kind: item::FuncKind, + leading_comments: Vec>, + sig: ParsedFuncSig<'_>, + body_span: Option, +) -> item::FunctionDef<'db> { + let func_name = sig.name.0; + let func_def = ctx.alloc_def_with_location(DefKind::Function, Some(func_name), span.start); + + let func_anchor = AnchorId::def(ctx.db, func_def); + let lowered_sig = lower_func_sig(ctx.db, func_anchor, span.start, sig); + let func_span = span_from_absolute(func_anchor, span, span.start); + + let body = body_span.map(|body_span| { + let body_def = ctx.with_owner(func_def, |ctx| { + ctx.alloc_def_with_location(DefKind::FuncBody, Some(func_name), body_span.start) + }); + let body_anchor = AnchorId::def(ctx.db, body_def); + + let mut arenas = BodyArenas::new(); + let mut top_level_stmts = named_return_bindings(ctx.db, &lowered_sig) + .into_iter() + .map(|(name, ty)| { + arenas.alloc_stmt(function::Stmt { + span: name.span(ctx.db), + kind: function::StmtKind::Let { + comptime: None, + name, + ty: Some(ty), + init: None, + }, + }) + }) + .collect::>(); + top_level_stmts.extend(ctx.with_owner(body_def, |ctx| { + ctx.lower_body_statements(body_anchor, body_span, &mut arenas) + })); + let lowered_body_span = span_from_absolute(body_anchor, body_span, body_span.start); + let (stmts, exprs, pats) = arenas.into_parts(); + function::FuncBody::new( + ctx.db, + body_def, + lowered_body_span, + top_level_stmts, + stmts, + exprs, + pats, + ) + }); + + let leading_comments = lower_source_comments(leading_comments); + + item::FunctionDef::new( + ctx.db, + func_def, + func_span, + kind, + leading_comments, + lowered_sig, + body, + ) +} + +pub(super) fn lower_instance<'db>( + ctx: &mut LoweringCtx<'db, '_>, + meta: ParsedItemMeta<'_>, + type_vars: Vec>, + preds: Vec>, + default_kw: Option, + head: ParsedPred<'_>, + methods: Vec>, +) -> item::InstanceDef<'db> { + let ParsedItemMeta { + span, + leading_comments, + } = meta; + let instance_name = head.class.0; + let fingerprint = instance_head_fingerprint(&type_vars, &head); + let instance_def = ctx.alloc_def_with_fingerprint( + DefKind::Instance, + Some(instance_name), + fingerprint.as_deref(), + span.start, + ); + + let anchor = AnchorId::def(ctx.db, instance_def); + let type_vars = type_vars + .into_iter() + .map(|var| lower_spanned_ident(ctx.db, anchor, span.start, var)) + .collect::>(); + let preds = preds + .into_iter() + .map(|pred| lower_pred_ref(ctx.db, anchor, span.start, pred)) + .collect::>(); + let default_kw = default_kw.map(|kw_span| span_from_absolute(anchor, kw_span, span.start)); + let head = lower_pred_ref(ctx.db, anchor, span.start, head); + let methods = ctx.with_owner(instance_def, |ctx| { + methods + .into_iter() + .map(|method| { + lower_function( + ctx, + method.span, + method.kind, + method.leading_comments, + method.sig, + method.body_span, + ) + }) + .collect::>() + }); + let span = span_from_absolute(anchor, span, span.start); + + item::InstanceDef::new( + ctx.db, + instance_def, + span, + lower_source_comments(leading_comments), + type_vars, + preds, + default_kw, + head, + methods, + ) +} + +fn lower_contract_item<'db>( + ctx: &mut LoweringCtx<'db, '_>, + item: ParsedContractItem<'_>, +) -> item::ContractItem<'db> { + match item { + ParsedContractItem::Function(function) => item::ContractItem::FunctionDef(lower_function( + ctx, + function.span, + function.kind, + function.leading_comments, + function.sig, + function.body_span, + )), + ParsedContractItem::TypeAlias { + span, + leading_comments, + kind, + name, + ty_params, + ty, + } => item::ContractItem::TypeAlias(lower_type_alias( + ctx, + span, + leading_comments, + kind, + name, + ty_params, + ty, + )), + ParsedContractItem::Adt { + span, + leading_comments, + name, + ty_params, + ctors, + } => item::ContractItem::AdtDef(lower_adt( + ctx, + span, + leading_comments, + name, + ty_params, + ctors, + )), + ParsedContractItem::Error { + span, + leading_comments, + } => item::ContractItem::Error { + span: root_span_from_lex(ctx.db, ctx.file, span), + leading_comments: item::SourceComments::new( + ctx.db, + lower_source_comments(leading_comments), + ), + }, + } +} + +fn lower_field<'db>( + ctx: &mut LoweringCtx<'db, '_>, + anchor: AnchorId<'db>, + base_start: usize, + field: ParsedFieldDef<'_>, +) -> item::FieldDef<'db> { + let _field_span = field.span; + let name = lower_spanned_ident(ctx.db, anchor, base_start, field.name); + let ty = lower_type_ref(ctx.db, anchor, base_start, field.ty); + let init = field.init.map(|expr| { + let span = span_from_absolute(anchor, expr.span, base_start); + let mut arenas = BodyArenas::new(); + let root = ctx.lower_expr(anchor, base_start, expr, &mut arenas); + let (_, exprs, _) = arenas.into_parts(); + item::FieldInit::new(span, root, exprs) + }); + item::FieldDef::new(name, ty, init) +} + +pub(super) fn lower_contract<'db>( + ctx: &mut LoweringCtx<'db, '_>, + span: LexSpan, + leading_comments: Vec>, + kind: item::ContractKind, + name: SpannedStr<'_>, + ty_params: Vec>, + fields: Vec>, + items: Vec>, +) -> item::ContractDef<'db> { + let contract_def = ctx.alloc_def_with_location(DefKind::Contract, Some(name.0), span.start); + + let anchor = AnchorId::def(ctx.db, contract_def); + let name = lower_spanned_ident(ctx.db, anchor, span.start, name); + let ty_params = ty_params + .into_iter() + .map(|param| lower_spanned_ident(ctx.db, anchor, span.start, param)) + .collect::>(); + let (fields, field_comments, items) = ctx.with_owner(contract_def, |ctx| { + let (fields, field_comments) = fields + .into_iter() + .map(|mut field| { + let comments = lower_source_comments(std::mem::take(&mut field.leading_comments)); + let field = lower_field(ctx, anchor, span.start, field); + (field, comments) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + let items = items + .into_iter() + .map(|item| lower_contract_item(ctx, item)) + .collect::>(); + (fields, field_comments, items) + }); + let span = span_from_absolute(anchor, span, span.start); + + item::ContractDef::new( + ctx.db, + contract_def, + span, + lower_source_comments(leading_comments), + kind, + name, + ty_params, + fields, + field_comments, + items, + ) +} diff --git a/crates/parser/src/lower/mod.rs b/crates/parser/src/lower/mod.rs new file mode 100644 index 00000000..4d1cd85c --- /dev/null +++ b/crates/parser/src/lower/mod.rs @@ -0,0 +1,253 @@ +//! Lowering from parsed syntax into HIR. +//! +//! Lowering is where source-level parsed DTOs gain HIR identity. It allocates +//! structural `DefId`s, records def-anchor base offsets, converts absolute +//! lexical spans into anchor-relative spans, and builds function-body arenas. +//! This is also where parse errors become pull-style diagnostics. + +mod body; +mod context; +mod fingerprint; +mod items; +mod span; +mod yul; + +use hir::{ + anchor::{DefKind, DefLocation, DefLocationTable, KeyCanonicalizer}, + ast::item, + diag::Offset, + input::SourceFile, + span::{AnchorId, Span}, +}; + +use self::{ + context::LoweringCtx, + items::{ + lower_adt, lower_class, lower_contract, lower_export, lower_function, lower_import, + lower_instance, lower_parse_errors, lower_pragma, lower_type_alias, + }, + span::{offset_from_usize, root_span_from_lex}, +}; +use crate::{Db, ParseHirOutput, parse::parse_supported_items, types::*}; + +/// Parses and lowers one source file into HIR. +/// +/// The returned `ParseHirOutput` contains both the lowered module and the +/// def-location table required for later absolute span resolution. This +/// function assumes parsed spans are absolute byte offsets into the same source +/// file. +/// +/// # Panics +/// +/// Panics if a parsed span cannot fit into the compact `Offset` representation +/// or if lowering observes a span that starts before its chosen anchor base. +pub(crate) fn parse_file_to_hir_impl<'db>( + db: &'db dyn Db, + file: SourceFile, +) -> ParseHirOutput<'db> { + let mut keys = KeyCanonicalizer::new(); + let module_def = keys.alloc_def(db, file, None, DefKind::Module, None, None); + + let source = file.content(db).as_deref().unwrap_or(""); + let end = offset_from_usize(source.len()); + let module_span = Span::new(AnchorId::root(db, file), Offset::new(0), end); + + let mut items = Vec::new(); + let mut def_locations = vec![( + module_def, + DefLocation { + file, + base_offset: Offset::new(0), + }, + )]; + + let parsed_items = parse_supported_items(source); + let mut parse_errors = parsed_items.errors; + tracing::debug!( + target: "parser", + items = parsed_items.output.len(), + errors = parse_errors.len(), + "lowering parsed file" + ); + + { + let mut ctx = LoweringCtx::new( + db, + file, + Some(module_def), + &mut keys, + &mut def_locations, + source, + &mut parse_errors, + ); + + for parsed in parsed_items.output { + match parsed { + ParsedTopItem::Import { + span, + leading_comments, + external, + path, + alias, + selector, + hiding, + } => { + let import = lower_import( + &mut ctx, + ParsedItemMeta { + span, + leading_comments, + }, + external, + path, + alias, + selector, + hiding, + ); + items.push(item::Item::Import(import)); + } + ParsedTopItem::Export { + span, + leading_comments, + kind, + } => { + let export = lower_export(&mut ctx, span, leading_comments, kind); + items.push(item::Item::Export(export)); + } + ParsedTopItem::Pragma { + span, + leading_comments, + name, + items: pragma_items, + } => { + let pragma = lower_pragma(&mut ctx, span, leading_comments, name, pragma_items); + items.push(item::Item::Pragma(pragma)); + } + ParsedTopItem::TypeAlias { + span, + leading_comments, + kind, + name, + ty_params, + ty, + } => { + let alias = lower_type_alias( + &mut ctx, + span, + leading_comments, + kind, + name, + ty_params, + ty, + ); + items.push(item::Item::TypeAlias(alias)); + } + ParsedTopItem::Adt { + span, + leading_comments, + name, + ty_params, + ctors, + } => { + let adt = lower_adt(&mut ctx, span, leading_comments, name, ty_params, ctors); + items.push(item::Item::AdtDef(adt)); + } + ParsedTopItem::Class { + span, + leading_comments, + type_vars, + super_preds, + head, + methods, + } => { + let class = lower_class( + &mut ctx, + span, + leading_comments, + type_vars, + super_preds, + head, + methods, + ); + items.push(item::Item::ClassDef(class)); + } + ParsedTopItem::Instance { + span, + leading_comments, + type_vars, + preds, + default_kw, + head, + methods, + } => { + let instance = lower_instance( + &mut ctx, + ParsedItemMeta { + span, + leading_comments, + }, + type_vars, + preds, + default_kw, + head, + methods, + ); + items.push(item::Item::InstanceDef(instance)); + } + ParsedTopItem::Contract { + span, + leading_comments, + kind, + name, + ty_params, + fields, + items: contract_items, + } => { + let contract = lower_contract( + &mut ctx, + span, + leading_comments, + kind, + name, + ty_params, + fields, + contract_items, + ); + items.push(item::Item::ContractDef(contract)); + } + ParsedTopItem::Function { + span, + leading_comments, + sig, + body_span, + } => { + let function = lower_function( + &mut ctx, + span, + item::FuncKind::Function, + leading_comments, + sig, + Some(body_span), + ); + items.push(item::Item::FunctionDef(function)); + } + ParsedTopItem::Error { + span, + leading_comments, + } => items.push(item::Item::Error { + span: root_span_from_lex(db, file, span), + leading_comments: item::SourceComments::new( + db, + items::lower_source_comments(leading_comments), + ), + }), + } + } + } + + let module = item::Module::new(db, module_def, module_span, items); + let def_locations = DefLocationTable::from_def_locations(def_locations); + let diagnostics = lower_parse_errors(db, file, parse_errors); + + ParseHirOutput::new(db, module, def_locations, diagnostics) +} diff --git a/crates/parser/src/lower/span.rs b/crates/parser/src/lower/span.rs new file mode 100644 index 00000000..a6a613bb --- /dev/null +++ b/crates/parser/src/lower/span.rs @@ -0,0 +1,116 @@ +use hir::{ + ast::Ident, + diag::Offset, + input::SourceFile, + span::{AnchorId, Span, SpannedElem}, +}; + +use crate::{Db, types::*}; + +pub(super) fn offset_from_usize(raw: usize) -> Offset { + Offset::try_from_usize(raw).expect("span offset exceeds u32::MAX") +} + +pub(super) fn span_from_absolute<'db>( + anchor: AnchorId<'db>, + abs: LexSpan, + base_start: usize, +) -> Span<'db> { + let rel_start = abs + .start + .checked_sub(base_start) + .expect("span start is before anchor base"); + let rel_end = abs + .end + .checked_sub(base_start) + .expect("span end is before anchor base"); + Span::new( + anchor, + offset_from_usize(rel_start), + offset_from_usize(rel_end), + ) +} + +pub(super) fn root_span_from_lex<'db>( + db: &'db dyn Db, + file: SourceFile, + span: LexSpan, +) -> Span<'db> { + Span::new( + AnchorId::root(db, file), + offset_from_usize(span.start), + offset_from_usize(span.end), + ) +} + +pub(super) fn lower_spanned_ident<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + (name, span): SpannedStr<'_>, +) -> SpannedElem<'db, Ident<'db>> { + SpannedElem::new( + Ident::new(db, name.to_owned()), + span_from_absolute(anchor, span, base_start), + ) +} + +pub(super) fn lower_owned_ident<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + name: String, + span: LexSpan, +) -> SpannedElem<'db, Ident<'db>> { + SpannedElem::new( + Ident::new(db, name), + span_from_absolute(anchor, span, base_start), + ) +} + +pub(super) fn path_text(path: &[SpannedStr<'_>]) -> String { + path.iter() + .map(|(name, _)| *name) + .collect::>() + .join(".") +} + +fn path_span(path: &[SpannedStr<'_>]) -> LexSpan { + let first = path.first().expect("qualified path is non-empty").1; + let last = path.last().expect("qualified path is non-empty").1; + LexSpan::from(first.start..last.end) +} + +fn lower_spanned_path_ident<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + path: Vec>, +) -> SpannedElem<'db, Ident<'db>> { + let span = path_span(&path); + lower_owned_ident(db, anchor, base_start, path_text(&path), span) +} + +pub(super) fn lower_qualifier_path<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + qualifiers: Vec>, +) -> Option>> { + if qualifiers.is_empty() { + None + } else { + Some(lower_spanned_path_ident(db, anchor, base_start, qualifiers)) + } +} + +pub(super) fn lower_path<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + path: Vec>, +) -> Vec>> { + path.into_iter() + .map(|segment| lower_spanned_ident(db, anchor, base_start, segment)) + .collect() +} diff --git a/crates/parser/src/lower/yul.rs b/crates/parser/src/lower/yul.rs new file mode 100644 index 00000000..2a3e869b --- /dev/null +++ b/crates/parser/src/lower/yul.rs @@ -0,0 +1,147 @@ +use hir::{ast::function, span::AnchorId}; + +use super::span::{lower_spanned_ident, span_from_absolute}; +use crate::{Db, types::*}; + +fn lower_parsed_yul_lit(lit: ParsedYulLitKind<'_>) -> function::YulLitKind { + match lit { + ParsedYulLitKind::Number(n) => function::YulLitKind::Number(n.to_owned()), + ParsedYulLitKind::Hex(h) => function::YulLitKind::Hex(h.to_owned()), + ParsedYulLitKind::String(s) => function::YulLitKind::String(s.to_owned()), + ParsedYulLitKind::Bool(b) => function::YulLitKind::Bool(b), + } +} + +fn lower_parsed_yul_expr<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + expr: ParsedYulExpr<'_>, +) -> function::YulExpr<'db> { + let span = span_from_absolute(anchor, expr.span, base_start); + let kind = match expr.kind { + ParsedYulExprKind::Lit(lit) => function::YulExprKind::Lit(lower_parsed_yul_lit(lit)), + ParsedYulExprKind::Ident(name) => { + function::YulExprKind::Ident(lower_spanned_ident(db, anchor, base_start, name)) + } + ParsedYulExprKind::Call { name, args } => { + let name = lower_spanned_ident(db, anchor, base_start, name); + let args = args + .into_iter() + .map(|arg| lower_parsed_yul_expr(db, anchor, base_start, arg)) + .collect(); + function::YulExprKind::Call { name, args } + } + ParsedYulExprKind::Error => function::YulExprKind::Error, + }; + function::YulExpr { span, kind } +} + +pub(super) fn lower_parsed_yul_stmt<'db>( + db: &'db dyn Db, + anchor: AnchorId<'db>, + base_start: usize, + stmt: ParsedYulStmt<'_>, +) -> function::YulStmt<'db> { + let span = span_from_absolute(anchor, stmt.span, base_start); + let kind = match stmt.kind { + ParsedYulStmtKind::Block(body) => function::YulStmtKind::Block( + body.into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + ), + ParsedYulStmtKind::Let { names, init } => function::YulStmtKind::Let { + names: names + .into_iter() + .map(|name| lower_spanned_ident(db, anchor, base_start, name)) + .collect(), + init: init.map(|expr| lower_parsed_yul_expr(db, anchor, base_start, expr)), + }, + ParsedYulStmtKind::Assign { names, value } => function::YulStmtKind::Assign { + names: names + .into_iter() + .map(|name| lower_spanned_ident(db, anchor, base_start, name)) + .collect(), + value: lower_parsed_yul_expr(db, anchor, base_start, value), + }, + ParsedYulStmtKind::Expr(expr) => { + function::YulStmtKind::Expr(lower_parsed_yul_expr(db, anchor, base_start, expr)) + } + ParsedYulStmtKind::If { cond, body } => function::YulStmtKind::If { + cond: lower_parsed_yul_expr(db, anchor, base_start, cond), + body: body + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + }, + ParsedYulStmtKind::For { + init, + cond, + post, + body, + } => function::YulStmtKind::For { + init: init + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + cond: lower_parsed_yul_expr(db, anchor, base_start, cond), + post: post + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + body: body + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + }, + ParsedYulStmtKind::Switch { + expr, + cases, + default, + } => function::YulStmtKind::Switch { + expr: lower_parsed_yul_expr(db, anchor, base_start, expr), + cases: cases + .into_iter() + .map(|case| function::YulCase { + span: span_from_absolute(anchor, case.span, base_start), + lit: lower_parsed_yul_lit(case.lit), + body: case + .body + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + }) + .collect(), + default: default.map(|body| { + body.into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect() + }), + }, + ParsedYulStmtKind::FunctionDef { + name, + params, + rets, + body, + } => function::YulStmtKind::FunctionDef { + name: lower_spanned_ident(db, anchor, base_start, name), + params: params + .into_iter() + .map(|param| lower_spanned_ident(db, anchor, base_start, param)) + .collect(), + rets: rets + .into_iter() + .map(|ret| lower_spanned_ident(db, anchor, base_start, ret)) + .collect(), + body: body + .into_iter() + .map(|stmt| lower_parsed_yul_stmt(db, anchor, base_start, stmt)) + .collect(), + }, + ParsedYulStmtKind::Leave => function::YulStmtKind::Leave, + ParsedYulStmtKind::Break => function::YulStmtKind::Break, + ParsedYulStmtKind::Continue => function::YulStmtKind::Continue, + ParsedYulStmtKind::Error => function::YulStmtKind::Error, + }; + function::YulStmt { span, kind } +} diff --git a/crates/parser/src/parse.rs b/crates/parser/src/parse.rs deleted file mode 100644 index a9344cc2..00000000 --- a/crates/parser/src/parse.rs +++ /dev/null @@ -1,1962 +0,0 @@ -use chumsky::{input::ValueInput, prelude::*}; -use hir::ast::function; -use logos::Logos; - -use crate::{lexer::Token, types::*}; - -fn ident_parser<'src, I>() -> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - select! { - Token::Ident(name) => name, - Token::True => "true", - Token::False => "false", - } - .validate(|name, e, emitter| { - if name.contains('-') { - emitter.emit(Rich::custom( - e.span(), - format!("identifier `{name}` cannot contain hyphens"), - )); - } - (name, e.span()) - }) -} - -fn pragma_ident_parser<'src, I>() -> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - select! { Token::Ident(name) => name }.map_with(|name, e| (name, e.span())) -} - -fn import_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - just(Token::Import) - .ignore_then( - ident_parser() - .separated_by(just(Token::Dot)) - .at_least(1) - .collect::>(), - ) - .then_ignore(just(Token::Semi)) - .map_with(|path, e| ParsedTopItem::Import { - span: e.span(), - path, - }) - .labelled("import declaration") - .as_context() - .boxed() -} - -fn pragma_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let items = ident_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(); - - just(Token::Pragma) - .ignore_then(pragma_ident_parser()) - .then(items) - .then_ignore(just(Token::Semi)) - .map_with(|(name, items), e| ParsedTopItem::Pragma { - span: e.span(), - name, - items, - }) - .labelled("pragma declaration") - .as_context() - .boxed() -} - -fn type_parser<'src, I>() -> impl Parser<'src, I, ParsedTy<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|ty| { - let args = ty - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|args| args.unwrap_or_default()) - .boxed(); - - let named_type = ident_parser() - .then(args) - .map_with(|(name, args), e| ParsedTy { - span: e.span(), - kind: ParsedTyKind::Named { name, args }, - }) - .boxed(); - - let paren_types = ty - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .boxed(); - - let fn_type = paren_types - .clone() - .then_ignore(just(Token::Arrow)) - .then(ty.clone()) - .map_with(|(params, ret), e| ParsedTy { - span: e.span(), - kind: ParsedTyKind::Fn { - params, - ret: Box::new(ret), - }, - }) - .boxed(); - - let tuple_type = paren_types - .map_with(|elems, e| ParsedTy { - span: e.span(), - kind: ParsedTyKind::Tuple { elems }, - }) - .boxed(); - - fn_type.or(tuple_type).or(named_type) - }) - .labelled("type") - .as_context() -} - -fn pred_parser<'src, I>() -> impl Parser<'src, I, ParsedPred<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let class_args = type_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|args| args.unwrap_or_default()) - .boxed(); - - type_parser() - .then_ignore(just(Token::Colon)) - .then(ident_parser()) - .then(class_args) - .map(|((ty, class), args)| ParsedPred { ty, class, args }) - .labelled("predicate") - .as_context() - .boxed() -} - -fn pred_list_parser<'src, I>() -> impl Parser<'src, I, Vec>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let bare = pred_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .boxed(); - bare.clone() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or(bare) -} - -#[derive(Debug, Clone)] -enum ParsedForallBinder<'src> { - Var(SpannedStr<'src>), - Bound { - var: SpannedStr<'src>, - pred: ParsedPred<'src>, - }, -} - -fn forall_binder_parser<'src, I>() -> impl Parser<'src, I, ParsedForallBinder<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let class_args = type_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|args| args.unwrap_or_default()) - .boxed(); - - let bounded = ident_parser() - .then_ignore(just(Token::Colon)) - .then(ident_parser()) - .then(class_args) - .map(|((var, class), args)| { - let ty = ParsedTy { - span: var.1, - kind: ParsedTyKind::Named { - name: var, - args: Vec::new(), - }, - }; - let pred = ParsedPred { ty, class, args }; - ParsedForallBinder::Bound { var, pred } - }); - - let bare = ident_parser().map(ParsedForallBinder::Var); - - choice((bounded, bare)) -} - -fn forall_clause_parser<'src, I>() --> impl Parser<'src, I, (Vec>, Vec>), ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let binder = forall_binder_parser().boxed(); - let binders = binder - .clone() - .then( - just(Token::Comma) - .or_not() - .ignore_then(binder) - .repeated() - .collect::>(), - ) - .map(|(first, mut rest)| { - let mut all = Vec::with_capacity(rest.len() + 1); - all.push(first); - all.append(&mut rest); - all - }); - - just(Token::Forall) - .ignore_then(binders) - .then_ignore(just(Token::Dot)) - .or_not() - .map(|binders| { - let mut type_vars = Vec::new(); - let mut preds = Vec::new(); - if let Some(binders) = binders { - for binder in binders { - match binder { - ParsedForallBinder::Var(var) => type_vars.push(var), - ParsedForallBinder::Bound { var, pred } => { - type_vars.push(var); - preds.push(pred); - } - } - } - } - (type_vars, preds) - }) -} - -#[derive(Debug, Clone)] -enum ParsedPostfixOp<'src> { - Index(ParsedExpr<'src>), - Call(Vec>), - Field(SpannedStr<'src>), -} - -#[derive(Debug, Clone, Copy)] -enum ParsedAssignOp { - Eq, - AddEq, - SubEq, -} - -fn parsed_lit_parser<'src, I>() -> impl Parser<'src, I, ParsedLitKind<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - select! { - Token::Number(n) => ParsedLitKind::Number(n), - Token::HexLit(h) => ParsedLitKind::Hex(h), - Token::String(s) => ParsedLitKind::String(s), - } - .boxed() -} - -fn parsed_expr_parser<'src, I>() -> impl Parser<'src, I, ParsedExpr<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|expr| { - let lambda_param = ident_parser() - .then(just(Token::Colon).ignore_then(type_parser()).or_not()) - .map(|(name, ty)| match ty { - Some(ty) => ParsedFuncParam::Typed { name, ty }, - None => ParsedFuncParam::Untyped { name }, - }) - .boxed(); - - let lambda_params = lambda_param - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map_with(|params, e| (params, e.span())) - .boxed(); - - let lambda_expr = just(Token::Lam) - .ignore_then(lambda_params) - .then(just(Token::Arrow).ignore_then(type_parser()).or_not()) - .then(body_span_parser()) - .map_with(|(((params, params_span), ret), body_span), e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::Lambda { - params, - params_span, - ret, - body_span, - }, - }) - .boxed(); - - let if_expr = just(Token::If) - .ignore_then(expr.clone()) - .then_ignore(just(Token::Then)) - .then(expr.clone()) - .then_ignore(just(Token::Else)) - .then(expr.clone()) - .map_with(|((cond, then_expr), else_expr), e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::If { - cond: Box::new(cond), - then_expr: Box::new(then_expr), - else_expr: Box::new(else_expr), - }, - }) - .boxed(); - - let boundary = just(Token::Semi) - .or(just(Token::Comma)) - .or(just(Token::RParen)) - .or(just(Token::RBracket)) - .or(just(Token::RBrace)) - .or(just(Token::Then)) - .or(just(Token::Else)) - .or(just(Token::FatArrow)) - .or(just(Token::Pipe)); - let atom_recovery = any() - .and_is(boundary.not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::Error, - }); - - let tuple_or_paren_expr = expr - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map_with(|elems, e| { - if elems.len() == 1 { - elems.into_iter().next().expect("len == 1") - } else { - ParsedExpr { - span: e.span(), - kind: ParsedExprKind::Error, - } - } - }) - .boxed(); - - let atom = parsed_lit_parser() - .map_with(|lit, e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::Lit(lit), - }) - .or(ident_parser().map(|ident| ParsedExpr { - span: ident.1, - kind: ParsedExprKind::Ident(ident), - })) - .or(tuple_or_paren_expr) - .or(lambda_expr) - .or(if_expr) - .recover_with(via_parser(atom_recovery)) - .boxed(); - - let index_op = expr - .clone() - .delimited_by(just(Token::LBracket), just(Token::RBracket)) - .map(ParsedPostfixOp::Index); - let call_op = expr - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map(ParsedPostfixOp::Call); - let field_op = just(Token::Dot) - .ignore_then(ident_parser()) - .map(ParsedPostfixOp::Field); - - let postfix = atom - .foldl_with( - index_op.or(call_op).or(field_op).repeated(), - |base, op, e| ParsedExpr { - span: e.span(), - kind: match op { - ParsedPostfixOp::Index(index) => ParsedExprKind::Index { - base: Box::new(base), - index: Box::new(index), - }, - ParsedPostfixOp::Call(args) => ParsedExprKind::Call { - callee: Box::new(base), - args, - }, - ParsedPostfixOp::Field(field) => ParsedExprKind::Field { - base: Box::new(base), - field, - }, - }, - }, - ) - .boxed(); - - let unary_op = just(Token::Bang) - .to(function::UnOp::Not) - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let unary = unary_op - .repeated() - .foldr_with(postfix, |op, expr, e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::UnaryOp { - op, - expr: Box::new(expr), - }, - }) - .boxed(); - - let mul_op = select! { - Token::Star => function::BinOp::Mul, - Token::Slash => function::BinOp::Div, - Token::Percent => function::BinOp::Mod, - } - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let mul = unary.clone().foldl_with( - mul_op.then(unary.clone()).repeated(), - |lhs, (op, rhs), e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - }, - ); - - let add_op = select! { - Token::Plus => function::BinOp::Add, - Token::Minus => function::BinOp::Sub, - } - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let add = mul - .clone() - .foldl_with(add_op.then(mul).repeated(), |lhs, (op, rhs), e| { - ParsedExpr { - span: e.span(), - kind: ParsedExprKind::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - } - }); - - let cmp_op = select! { - Token::EqEq => function::BinOp::Eq, - Token::NotEq => function::BinOp::NotEq, - Token::Less => function::BinOp::Lt, - Token::Greater => function::BinOp::Gt, - Token::LessEq => function::BinOp::LtEq, - Token::GreaterEq => function::BinOp::GtEq, - } - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let cmp = add - .clone() - .foldl_with(cmp_op.then(add).repeated(), |lhs, (op, rhs), e| { - ParsedExpr { - span: e.span(), - kind: ParsedExprKind::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - } - }); - - let and_op = just(Token::AndAnd) - .to(function::BinOp::And) - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let and = cmp - .clone() - .foldl_with(and_op.then(cmp).repeated(), |lhs, (op, rhs), e| { - ParsedExpr { - span: e.span(), - kind: ParsedExprKind::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - } - }); - - let or_op = just(Token::OrOr) - .to(function::BinOp::Or) - .map_with(|op, e| ParsedSpanned::new(op, e.span())); - let or = and - .clone() - .foldl_with(or_op.then(and).repeated(), |lhs, (op, rhs), e| ParsedExpr { - span: e.span(), - kind: ParsedExprKind::BinOp { - lhs: Box::new(lhs), - op, - rhs: Box::new(rhs), - }, - }); - - let type_annot = just(Token::Colon).ignore_then(type_parser()).or_not(); - or.then(type_annot) - .map_with(|(expr, ty), e| match ty { - Some(ty) => ParsedExpr { - span: e.span(), - kind: ParsedExprKind::TypeAnnot { - expr: Box::new(expr), - ty, - }, - }, - None => expr, - }) - .boxed() - }) - .labelled("expression") -} - -fn parsed_pat_parser<'src, I>() -> impl Parser<'src, I, ParsedPat<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|pat| { - let wildcard = just(Token::Underscore) - .map_with(|_, e| ParsedPat { - span: e.span(), - kind: ParsedPatKind::Wildcard, - }) - .boxed(); - - let lit_pat = parsed_lit_parser() - .map_with(|lit, e| ParsedPat { - span: e.span(), - kind: ParsedPatKind::Lit(lit), - }) - .boxed(); - - let tuple_pat = pat - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map_with(|pats, e| ParsedPat { - span: e.span(), - kind: ParsedPatKind::Tuple(pats), - }) - .boxed(); - - let ctor_args = pat - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .boxed(); - - let ctor_or_var = ident_parser() - .then(ctor_args) - .map_with(|(name, args), e| ParsedPat { - span: e.span(), - kind: match args { - Some(args) => ParsedPatKind::Ctor { name, args }, - None => ParsedPatKind::Var(name), - }, - }) - .boxed(); - - let boundary = just(Token::Comma) - .or(just(Token::RParen)) - .or(just(Token::FatArrow)) - .or(just(Token::Pipe)) - .or(just(Token::RBrace)); - let recovery = any() - .and_is(boundary.not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedPat { - span: e.span(), - kind: ParsedPatKind::Error, - }); - - wildcard - .or(lit_pat) - .or(tuple_pat) - .or(ctor_or_var) - .recover_with(via_parser(recovery)) - }) - .labelled("pattern") -} - -fn parsed_yul_lit_parser<'src, I>() -> impl Parser<'src, I, ParsedYulLitKind<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - select! { - Token::Number(n) => ParsedYulLitKind::Number(n), - Token::HexLit(h) => ParsedYulLitKind::Hex(h), - Token::String(s) => ParsedYulLitKind::String(s), - Token::True => ParsedYulLitKind::Bool(true), - Token::False => ParsedYulLitKind::Bool(false), - } - .boxed() -} - -fn parsed_yul_expr_parser<'src, I>() -> impl Parser<'src, I, ParsedYulExpr<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|expr| { - let lit = parsed_yul_lit_parser() - .map_with(|lit, e| ParsedYulExpr { - span: e.span(), - kind: ParsedYulExprKind::Lit(lit), - }) - .boxed(); - - let ident_or_call = ident_parser() - .then( - expr.clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not(), - ) - .map_with(|(name, args), e| ParsedYulExpr { - span: e.span(), - kind: match args { - Some(args) => ParsedYulExprKind::Call { name, args }, - None => ParsedYulExprKind::Ident(name), - }, - }) - .boxed(); - - let recovery = any() - .and_is( - just(Token::Comma) - .or(just(Token::RParen)) - .or(just(Token::RBrace)) - .not(), - ) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedYulExpr { - span: e.span(), - kind: ParsedYulExprKind::Error, - }); - - choice((lit, ident_or_call)).recover_with(via_parser(recovery)) - }) - .labelled("assembly expression") -} - -fn parsed_yul_stmt_parser<'src, I>() -> impl Parser<'src, I, ParsedYulStmt<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|stmt| { - let block = stmt - .clone() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .map_with(|body, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Block(body), - }) - .boxed(); - - let let_stmt = just(Token::Let) - .ignore_then( - ident_parser() - .separated_by(just(Token::Comma)) - .at_least(1) - .collect::>(), - ) - .then( - just(Token::ColonEq) - .ignore_then(parsed_yul_expr_parser()) - .or_not(), - ) - .map_with(|(names, init), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Let { names, init }, - }) - .boxed(); - - let assign = ident_parser() - .separated_by(just(Token::Comma)) - .at_least(1) - .collect::>() - .then_ignore(just(Token::ColonEq)) - .then(parsed_yul_expr_parser()) - .map_with(|(names, value), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Assign { names, value }, - }) - .boxed(); - - let expr_stmt = parsed_yul_expr_parser() - .map_with(|expr, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Expr(expr), - }) - .boxed(); - - let return_builtin = just(Token::Return) - .map_with(|_, e| ("return", e.span())) - .then( - parsed_yul_expr_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)), - ) - .map_with(|(name, args), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Expr(ParsedYulExpr { - span: e.span(), - kind: ParsedYulExprKind::Call { name, args }, - }), - }) - .boxed(); - - let if_stmt = just(Token::If) - .ignore_then(parsed_yul_expr_parser()) - .then( - stmt.clone() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)), - ) - .map_with(|(cond, body), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::If { cond, body }, - }) - .boxed(); - - let stmt_block = stmt - .clone() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)); - - let for_stmt = just(Token::For) - .ignore_then(stmt_block.clone()) - .then(parsed_yul_expr_parser()) - .then(stmt_block.clone()) - .then(stmt_block.clone()) - .map_with(|(((init, cond), post), body), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::For { - init, - cond, - post, - body, - }, - }) - .boxed(); - - let case = just(Token::Case) - .ignore_then(parsed_yul_lit_parser()) - .then(stmt_block.clone()) - .map_with(|(lit, body), e| ParsedYulCase { - span: e.span(), - lit, - body, - }); - let default = just(Token::Default).ignore_then(stmt_block.clone()); - let switch_stmt = just(Token::Switch) - .ignore_then(parsed_yul_expr_parser()) - .then(case.repeated().collect::>()) - .then(default.or_not()) - .map_with(|((expr, cases), default), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Switch { - expr, - cases, - default, - }, - }) - .boxed(); - - let ident_list = ident_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)); - let rets = just(Token::Arrow) - .ignore_then( - ident_parser() - .separated_by(just(Token::Comma)) - .at_least(1) - .collect::>(), - ) - .or_not() - .map(|r| r.unwrap_or_default()); - let function_def = just(Token::Function) - .ignore_then(ident_parser()) - .then(ident_list) - .then(rets) - .then(stmt_block) - .map_with(|(((name, params), rets), body), e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::FunctionDef { - name, - params, - rets, - body, - }, - }) - .boxed(); - - let leave = just(Token::Leave).map_with(|_, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Leave, - }); - let break_ = just(Token::Break).map_with(|_, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Break, - }); - let continue_ = just(Token::Continue).map_with(|_, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Continue, - }); - - let recovery = any() - .and_is(just(Token::RBrace).not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedYulStmt { - span: e.span(), - kind: ParsedYulStmtKind::Error, - }); - - choice(( - block, - let_stmt, - if_stmt, - for_stmt, - switch_stmt, - function_def, - assign, - return_builtin, - leave, - break_, - continue_, - expr_stmt, - )) - .then_ignore(just(Token::Semi).or_not()) - .recover_with(via_parser(recovery)) - }) - .labelled("assembly statement") -} - -fn parsed_stmt_parser<'src, I>() -> impl Parser<'src, I, ParsedStmt<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - recursive(|stmt| { - let match_arm = just(Token::Pipe) - .ignore_then( - parsed_pat_parser() - .separated_by(just(Token::Comma)) - .at_least(1) - .collect::>(), - ) - .then_ignore(just(Token::FatArrow)) - .then(stmt.clone().repeated().collect::>()) - .map_with(|(pats, body), e| ParsedMatchArm { - span: e.span(), - pats, - body, - }) - .boxed(); - - let let_stmt = just(Token::Let) - .ignore_then(ident_parser()) - .then(just(Token::Colon).ignore_then(type_parser()).or_not()) - .then( - just(Token::Eq) - .or(just(Token::ColonEq)) - .ignore_then(parsed_expr_parser()) - .or_not(), - ) - .then_ignore(just(Token::Semi)) - .map_with(|((name, ty), init), e| ParsedStmt { - span: e.span(), - kind: ParsedStmtKind::Let { name, ty, init }, - }) - .boxed(); - - let return_stmt = just(Token::Return) - .ignore_then(parsed_expr_parser().or_not()) - .then_ignore(just(Token::Semi)) - .map_with(|expr, e| ParsedStmt { - span: e.span(), - kind: ParsedStmtKind::Return(expr), - }) - .boxed(); - - let match_stmt = just(Token::Match) - .ignore_then( - parsed_expr_parser() - .separated_by(just(Token::Comma)) - .at_least(1) - .collect::>(), - ) - .then( - match_arm - .repeated() - .at_least(1) - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)), - ) - .map_with(|(scrutinees, arms), e| ParsedStmt { - span: e.span(), - kind: ParsedStmtKind::Match { scrutinees, arms }, - }) - .boxed(); - - let if_stmt = just(Token::If) - .ignore_then(parsed_expr_parser()) - .then( - stmt.clone() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)), - ) - .then( - just(Token::Else) - .ignore_then( - stmt.clone() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)), - ) - .or_not(), - ) - .map_with(|((cond, then_body), else_body), e| ParsedStmt { - span: e.span(), - kind: ParsedStmtKind::If { - cond, - then_body, - else_body, - }, - }) - .boxed(); - - let assembly_stmt = just(Token::Assembly) - .ignore_then( - parsed_yul_stmt_parser() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)), - ) - .map_with(|body, e| ParsedStmt { - span: e.span(), - kind: ParsedStmtKind::Assembly { body }, - }) - .boxed(); - - let assign_op = just(Token::Eq) - .to(ParsedAssignOp::Eq) - .or(just(Token::PlusEq).to(ParsedAssignOp::AddEq)) - .or(just(Token::MinusEq).to(ParsedAssignOp::SubEq)); - let assign_or_expr = parsed_expr_parser() - .then(assign_op.then(parsed_expr_parser()).or_not()) - .then_ignore(just(Token::Semi)) - .map_with(|(lhs, rhs), e| ParsedStmt { - span: e.span(), - kind: match rhs { - Some((ParsedAssignOp::Eq, rhs)) => ParsedStmtKind::Assign { lhs, rhs }, - Some((ParsedAssignOp::AddEq, rhs)) => ParsedStmtKind::AddAssign { lhs, rhs }, - Some((ParsedAssignOp::SubEq, rhs)) => ParsedStmtKind::SubAssign { lhs, rhs }, - None => ParsedStmtKind::Expr(lhs), - }, - }) - .boxed(); - - choice(( - let_stmt, - return_stmt, - match_stmt, - if_stmt, - assembly_stmt, - assign_or_expr, - )) - }) - .labelled("statement") -} - -fn param_parser<'src, I>() -> impl Parser<'src, I, ParsedFuncParam<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let typed = ident_parser() - .then_ignore(just(Token::Colon)) - .then(type_parser()) - .map(|(name, ty)| ParsedFuncParam::Typed { name, ty }) - .boxed(); - - let untyped = ident_parser() - .map(|name| ParsedFuncParam::Untyped { name }) - .boxed(); - - let recovery = any() - .and_is(just(Token::Comma).not()) - .and_is(just(Token::RParen).not()) - .repeated() - .at_least(1) - .to(ParsedFuncParam::Error); - - choice((typed, untyped)) - .recover_with(via_parser(recovery)) - .labelled("function parameter") - .as_context() -} - -fn signature_parser<'src, I>() -> impl Parser<'src, I, ParsedFuncSig<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let forall = forall_clause_parser().boxed(); - - let preds = pred_list_parser() - .then_ignore(just(Token::FatArrow)) - .or_not() - .map(|preds| preds.unwrap_or_default()) - .boxed(); - - let params = param_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map_with(|params, e| (params, e.span())) - .boxed(); - - let ret = just(Token::Arrow) - .ignore_then(type_parser()) - .or_not() - .boxed(); - - forall - .then(preds) - .then_ignore(just(Token::Function)) - .then(ident_parser()) - .then(params) - .then(ret) - .map_with( - |((((forall_info, mut preds), name), (params, params_span)), ret), e| { - let (type_vars, mut forall_preds) = forall_info; - forall_preds.append(&mut preds); - ParsedFuncSig { - span: e.span(), - type_vars, - preds: forall_preds, - name, - params, - params_span, - ret, - } - }, - ) - .labelled("function signature") - .as_context() - .boxed() -} - -fn body_span_parser<'src, I>() -> impl Parser<'src, I, LexSpan, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let body_contents = recursive(|body_contents| { - let nested = body_contents - .clone() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .ignored(); - - choice(( - nested, - any() - .and_is(just(Token::LBrace).not()) - .and_is(just(Token::RBrace).not()) - .ignored(), - )) - .repeated() - .ignored() - }); - - just(Token::LBrace) - .ignore_then(body_contents) - .then_ignore(just(Token::RBrace)) - .map_with(|_, e| e.span()) -} - -fn function_def_parser<'src, I>() -> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - signature_parser() - .then(body_span_parser()) - .map_with(|(sig, body_span), e| ParsedFunctionDef { - span: e.span(), - sig, - body_span, - }) - .labelled("function definition") - .as_context() - .boxed() -} - -fn constructor_def_parser<'src, I>() --> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let params = param_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map_with(|params, e| (params, e.span())) - .boxed(); - - let ret = just(Token::Arrow) - .ignore_then(type_parser()) - .or_not() - .boxed(); - - just(Token::Constructor) - .map_with(|_, e| e.span()) - .then(params) - .then(ret) - .then(body_span_parser()) - .map_with( - |(((name_span, (params, params_span)), ret), body_span), e| ParsedFunctionDef { - span: e.span(), - sig: ParsedFuncSig { - span: e.span(), - type_vars: Vec::new(), - preds: Vec::new(), - name: ("constructor", name_span), - params, - params_span, - ret, - }, - body_span, - }, - ) - .labelled("constructor definition") - .as_context() - .boxed() -} - -fn function_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - function_def_parser() - .map(|def| ParsedTopItem::Function { - span: def.span, - sig: def.sig, - body_span: def.body_span, - }) - .labelled("function declaration") - .as_context() - .boxed() -} - -fn type_alias_payload_parser<'src, I>() --> impl Parser<'src, I, (SpannedStr<'src>, Vec>, ParsedTy<'src>), ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let ty_params = ident_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|params| params.unwrap_or_default()) - .boxed(); - - let type_recovery = any() - .and_is(just(Token::Semi).not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedTy { - span: e.span(), - kind: ParsedTyKind::Error, - }); - - just(Token::Type) - .ignore_then(ident_parser()) - .then(ty_params) - .then_ignore(just(Token::Eq)) - .then(type_parser().recover_with(via_parser(type_recovery))) - .then_ignore(just(Token::Semi)) - .map(|((name, ty_params), ty)| (name, ty_params, ty)) -} - -fn type_alias_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - type_alias_payload_parser() - .map_with(|(name, ty_params, ty), e| ParsedTopItem::TypeAlias { - span: e.span(), - name, - ty_params, - ty, - }) - .labelled("type alias declaration") - .as_context() - .boxed() -} - -fn data_ctor_parser<'src, I>() -> impl Parser<'src, I, ParsedAdtCtor<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let fields = type_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|fields| fields.unwrap_or_default()); - - ident_parser() - .then(fields) - .map_with(|(name, fields), e| ParsedAdtCtor { - span: e.span(), - name, - fields, - }) - .boxed() -} - -fn adt_payload_parser<'src, I>() -> impl Parser< - 'src, - I, - ( - SpannedStr<'src>, - Vec>, - Vec>, - ), - ParserErr<'src>, -> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let ty_params = ident_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|params| params.unwrap_or_default()) - .boxed(); - - let ctors = just(Token::Eq) - .ignore_then( - data_ctor_parser() - .separated_by(just(Token::Pipe)) - .at_least(1) - .collect::>(), - ) - .or_not() - .map(|ctors| ctors.unwrap_or_default()) - .boxed(); - - just(Token::Data) - .ignore_then(ident_parser()) - .then(ty_params) - .then(ctors) - .then_ignore(just(Token::Semi)) - .map(|((name, ty_params), ctors)| (name, ty_params, ctors)) -} - -fn adt_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - adt_payload_parser() - .map_with(|(name, ty_params, ctors), e| ParsedTopItem::Adt { - span: e.span(), - name, - ty_params, - ctors, - }) - .labelled("data declaration") - .as_context() - .boxed() -} - -fn method_sig_parser<'src, I>() -> impl Parser<'src, I, ParsedFuncSig<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - signature_parser().then_ignore(just(Token::Semi)).boxed() -} - -fn class_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let forall = forall_clause_parser().boxed(); - - let super_preds = pred_list_parser() - .then_ignore(just(Token::FatArrow)) - .or_not() - .map(|preds| preds.unwrap_or_default()) - .boxed(); - - let methods = method_sig_parser() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .boxed(); - - forall - .then(super_preds) - .then_ignore(just(Token::Class)) - .then(pred_parser()) - .then(methods) - .map_with(|(((forall_info, mut super_preds), head), methods), e| { - let (type_vars, mut forall_preds) = forall_info; - forall_preds.append(&mut super_preds); - ParsedTopItem::Class { - span: e.span(), - type_vars, - super_preds: forall_preds, - head, - methods, - } - }) - .labelled("class declaration") - .as_context() - .boxed() -} - -fn instance_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let forall = forall_clause_parser().boxed(); - - let preds = pred_list_parser() - .then_ignore(just(Token::FatArrow)) - .or_not() - .map(|preds| preds.unwrap_or_default()) - .boxed(); - - let default_kw = just(Token::Default) - .map_with(|_, e| e.span()) - .or_not() - .boxed(); - - let methods = function_def_parser() - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .boxed(); - - let pre_instance_preds = forall - .clone() - .then(preds.clone()) - .then(default_kw.clone()) - .then_ignore(just(Token::Instance)) - .then(pred_parser()) - .then(methods.clone()) - .map_with( - |((((forall_info, mut preds), default_kw), head), methods), e| { - let (type_vars, mut forall_preds) = forall_info; - forall_preds.append(&mut preds); - ParsedTopItem::Instance { - span: e.span(), - type_vars, - preds: forall_preds, - default_kw, - head, - methods, - } - }, - ) - .boxed(); - - let post_instance_preds = forall - .then(default_kw) - .then_ignore(just(Token::Instance)) - .then(preds) - .then(pred_parser()) - .then(methods) - .map_with( - |((((forall_info, default_kw), mut preds), head), methods), e| { - let (type_vars, mut forall_preds) = forall_info; - forall_preds.append(&mut preds); - ParsedTopItem::Instance { - span: e.span(), - type_vars, - preds: forall_preds, - default_kw, - head, - methods, - } - }, - ) - .boxed(); - - choice((pre_instance_preds, post_instance_preds)) - .labelled("instance declaration") - .as_context() - .boxed() -} - -fn field_def_parser<'src, I>() -> impl Parser<'src, I, ParsedFieldDef<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - ident_parser() - .then_ignore(just(Token::Colon)) - .then(type_parser()) - .then_ignore(just(Token::Semi)) - .map_with(|(name, ty), e| ParsedFieldDef { - span: e.span(), - name, - ty, - }) - .labelled("contract field") - .as_context() - .boxed() -} - -fn contract_item_parser<'src, I>() -> impl Parser<'src, I, ParsedContractItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let function_def = function_def_parser() - .map(ParsedContractItem::Function) - .boxed(); - let constructor_def = constructor_def_parser() - .map(ParsedContractItem::Function) - .boxed(); - - let type_alias = type_alias_payload_parser() - .map_with(|(name, ty_params, ty), e| ParsedContractItem::TypeAlias { - span: e.span(), - name, - ty_params, - ty, - }) - .boxed(); - - let adt_def = adt_payload_parser() - .map_with(|(name, ty_params, ctors), e| ParsedContractItem::Adt { - span: e.span(), - name, - ty_params, - ctors, - }) - .boxed(); - - let item_start = just(Token::Function) - .or(just(Token::Constructor)) - .or(just(Token::Type)) - .or(just(Token::Data)) - .or(just(Token::RBrace)); - let recovery = any() - .and_is(item_start.not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedContractItem::Error { span: e.span() }); - - choice((function_def, constructor_def, type_alias, adt_def)) - .recover_with(via_parser(recovery)) - .labelled("contract member") - .as_context() -} - -fn contract_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let ty_params = ident_parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|params| params.unwrap_or_default()) - .boxed(); - - let fields = field_def_parser().repeated().collect::>().boxed(); - let items = contract_item_parser() - .repeated() - .collect::>() - .boxed(); - let body = fields - .then(items) - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .boxed(); - - just(Token::Contract) - .ignore_then(ident_parser()) - .then(ty_params) - .then(body) - .map_with( - |((name, ty_params), (fields, items)), e| ParsedTopItem::Contract { - span: e.span(), - name, - ty_params, - fields, - items, - }, - ) - .labelled("contract declaration") - .as_context() - .boxed() -} - -fn top_item_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> -where - I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, -{ - let item_start = just(Token::Import) - .or(just(Token::Pragma)) - .or(just(Token::Type)) - .or(just(Token::Data)) - .or(just(Token::Class)) - .or(just(Token::Instance)) - .or(just(Token::Contract)) - .or(just(Token::Function)) - .or(just(Token::Forall)) - .or(just(Token::Default)); - let recovery = any() - .and_is(item_start.not()) - .repeated() - .at_least(1) - .map_with(|_, e| ParsedTopItem::Error { span: e.span() }); - - choice(( - import_parser(), - pragma_parser(), - type_alias_parser(), - adt_parser(), - class_parser(), - instance_parser(), - contract_parser(), - function_parser(), - )) - .recover_with(via_parser(recovery)) - .labelled("top-level item") - .as_context() -} - -fn tokenize<'src>(src: &'src str) -> (Vec<(Token<'src>, LexSpan)>, Vec) { - let mut tokens = Vec::new(); - let mut errors = Vec::new(); - - for (tok, span) in Token::lexer(src).spanned() { - let message = invalid_token_message(src, span.start, span.end); - let span = LexSpan::from(span); - match tok { - Ok(tok) => tokens.push((tok, span)), - Err(()) => errors.push(ParsedError { span, message }), - } - } - - (tokens, errors) -} - -fn invalid_token_message(source: &str, start: usize, end: usize) -> String { - let snippet = source.get(start..end).unwrap_or(""); - if snippet.is_empty() { - "invalid token".to_owned() - } else { - format!("invalid token `{snippet}`") - } -} - -fn token_spelling(token: &Token<'_>) -> &'static str { - match token { - Token::Contract => "contract", - Token::Import => "import", - Token::Let => "let", - Token::Data => "data", - Token::Class => "class", - Token::Forall => "forall", - Token::Instance => "instance", - Token::If => "if", - Token::Else => "else", - Token::For => "for", - Token::Switch => "switch", - Token::Type => "type", - Token::Case => "case", - Token::Default => "default", - Token::Match => "match", - Token::Function => "function", - Token::Constructor => "constructor", - Token::Return => "return", - Token::Leave => "leave", - Token::Continue => "continue", - Token::Break => "break", - Token::Lam => "lam", - Token::Assembly => "assembly", - Token::Pragma => "pragma", - Token::Then => "then", - Token::True => "true", - Token::False => "false", - Token::ColonEq => ":=", - Token::Arrow => "->", - Token::FatArrow => "=>", - Token::EqEq => "==", - Token::NotEq => "!=", - Token::GreaterEq => ">=", - Token::LessEq => "<=", - Token::AndAnd => "&&", - Token::OrOr => "||", - Token::PlusEq => "+=", - Token::MinusEq => "-=", - Token::Plus => "+", - Token::Minus => "-", - Token::Star => "*", - Token::Slash => "/", - Token::Percent => "%", - Token::Bang => "!", - Token::Less => "<", - Token::Greater => ">", - Token::Eq => "=", - Token::Pipe => "|", - Token::Dot => ".", - Token::Colon => ":", - Token::Semi => ";", - Token::Comma => ",", - Token::LParen => "(", - Token::RParen => ")", - Token::LBrace => "{", - Token::RBrace => "}", - Token::LBracket => "[", - Token::RBracket => "]", - Token::Underscore => "_", - Token::LineComment => "//", - Token::BlockComment => "/* */", - Token::Ident(_) => "identifier", - Token::HexLit(_) => "hex literal", - Token::Number(_) => "number literal", - Token::String(_) => "string literal", - } -} - -fn token_found_description(token: &Token<'_>) -> String { - match token { - Token::Ident(name) => format!("identifier `{name}`"), - Token::Number(value) => format!("number literal `{value}`"), - Token::HexLit(value) => format!("hex literal `{value}`"), - Token::String(value) => format!("string literal {value}"), - _ => format!("`{}`", token_spelling(token)), - } -} - -fn token_expected_description(token: &Token<'_>) -> String { - match token { - Token::Ident(_) => "identifier".to_owned(), - Token::Number(_) => "number literal".to_owned(), - Token::HexLit(_) => "hex literal".to_owned(), - Token::String(_) => "string literal".to_owned(), - _ => format!("`{}`", token_spelling(token)), - } -} - -fn expected_pattern_description(pattern: &chumsky::error::RichPattern<'_, Token<'_>>) -> String { - match pattern { - chumsky::error::RichPattern::Token(token) => token_expected_description(token), - chumsky::error::RichPattern::Label(label) => label.to_string(), - chumsky::error::RichPattern::Identifier(name) => { - format!("identifier `{}`", name.trim_matches('"')) - } - chumsky::error::RichPattern::Any => "token".to_owned(), - chumsky::error::RichPattern::SomethingElse => "different token".to_owned(), - chumsky::error::RichPattern::EndOfInput => "end of input".to_owned(), - _ => "token".to_owned(), - } -} - -fn format_expected_list(expected: &[chumsky::error::RichPattern<'_, Token<'_>>]) -> String { - let mut items = expected - .iter() - .map(expected_pattern_description) - .collect::>(); - let has_specific = items - .iter() - .any(|item| item != "token" && item != "different token"); - if has_specific { - items.retain(|item| item != "token" && item != "different token"); - } - items.sort_unstable(); - items.dedup(); - - match items.as_slice() { - [] => "something else".to_owned(), - [single] => single.clone(), - _ => { - let last = items.pop().expect("non-empty list has a last element"); - format!("{}, or {last}", items.join(", ")) - } - } -} - -fn expected_found_message( - expected: &[chumsky::error::RichPattern<'_, Token<'_>>], - found: Option<&Token<'_>>, -) -> String { - let expected_text = format_expected_list(expected); - match found { - Some(found) => format!( - "unexpected {}; expected {expected_text}", - token_found_description(found) - ), - None => format!("unexpected end of input; expected {expected_text}"), - } -} - -fn parser_context(error: &Rich<'_, Token<'_>, LexSpan>) -> Option { - error.contexts().find_map(|(pattern, _)| match pattern { - chumsky::error::RichPattern::Label(label) => Some(label.to_string()), - _ => None, - }) -} - -fn parse_error_from_rich<'src>(error: Rich<'src, Token<'src>, LexSpan>) -> ParsedError { - let base_message = match error.reason() { - chumsky::error::RichReason::Custom(msg) => msg.clone(), - chumsky::error::RichReason::ExpectedFound { expected, found } => { - expected_found_message(expected, found.as_deref()) - } - }; - let message = match parser_context(&error) { - Some(ctx) => format!("{base_message} while parsing {ctx}"), - None => base_message, - }; - ParsedError { - span: *error.span(), - message, - } -} - -fn preview_span_source(source: &str, span: LexSpan, max_chars: usize) -> Option { - let snippet = source.get(span.start..span.end)?.trim(); - if snippet.is_empty() { - return None; - } - - let single_line = snippet.replace('\n', " "); - let compact = single_line.split_whitespace().collect::>().join(" "); - if compact.is_empty() { - return None; - } - - let mut preview = compact.chars().take(max_chars).collect::(); - if compact.chars().count() > max_chars { - preview.push_str("..."); - } - Some(preview) -} - -fn top_level_recovery_message(source: &str, span: LexSpan) -> String { - let expected = - "`import`, `pragma`, `type`, `data`, `class`, `instance`, `contract`, or `function`"; - match preview_span_source(source, span, 48) { - Some(preview) => format!( - "could not parse top-level item near `{preview}`; expected a declaration starting with {expected}" - ), - None => format!( - "could not parse top-level item; expected a declaration starting with {expected}" - ), - } -} - -fn span_contains(outer: LexSpan, inner: LexSpan) -> bool { - outer.start <= inner.start && inner.end <= outer.end -} - -pub(crate) fn parse_supported_items<'src>(src: &'src str) -> ParseOutput> { - let (tokens, mut errors) = tokenize(src); - let stream = chumsky::input::Stream::from_iter(tokens) - .map((0..src.len()).into(), |(tok, span): (_, _)| (tok, span)); - - let (output, parse_errors) = top_item_parser() - .repeated() - .collect::>() - .parse(stream) - .into_output_errors(); - - let output = output.unwrap_or_default(); - let recovery_spans = output - .iter() - .filter_map(|item| match item { - ParsedTopItem::Error { span } => Some(*span), - _ => None, - }) - .collect::>(); - - errors.extend( - parse_errors - .into_iter() - .map(parse_error_from_rich) - .filter(|err| { - !recovery_spans - .iter() - .any(|recovery| span_contains(*recovery, err.span)) - }), - ); - errors.extend(recovery_spans.into_iter().map(|span| ParsedError { - span, - message: top_level_recovery_message(src, span), - })); - - ParseOutput { output, errors } -} - -fn tokenize_with_base<'src>( - src: &'src str, - base_offset: usize, -) -> (Vec<(Token<'src>, LexSpan)>, Vec) { - let mut tokens = Vec::new(); - let mut errors = Vec::new(); - - for (tok, span) in Token::lexer(src).spanned() { - let message = invalid_token_message(src, span.start, span.end); - let span = LexSpan::from((span.start + base_offset)..(span.end + base_offset)); - match tok { - Ok(tok) => tokens.push((tok, span)), - Err(()) => errors.push(ParsedError { span, message }), - } - } - - (tokens, errors) -} - -pub(crate) fn parse_body_statements<'src>( - source: &'src str, - body_span: LexSpan, -) -> ParseOutput> { - if body_span.end <= body_span.start + 2 { - return ParseOutput { - output: Vec::new(), - errors: Vec::new(), - }; - } - - let inner_start = body_span.start + 1; - let inner_end = body_span.end - 1; - let Some(inner_source) = source.get(inner_start..inner_end) else { - return ParseOutput { - output: vec![ParsedStmt { - span: body_span, - kind: ParsedStmtKind::Error, - }], - errors: vec![ParsedError { - span: body_span, - message: "invalid function body span".to_owned(), - }], - }; - }; - - let (tokens, mut errors) = tokenize_with_base(inner_source, inner_start); - let stream = chumsky::input::Stream::from_iter(tokens) - .map((inner_start..inner_end).into(), |(tok, span): (_, _)| { - (tok, span) - }); - let (output, parse_errors) = parsed_stmt_parser() - .repeated() - .collect::>() - .parse(stream) - .into_output_errors(); - errors.extend(parse_errors.into_iter().map(parse_error_from_rich)); - - ParseOutput { - output: output.unwrap_or_default(), - errors, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn yul_call_in_assignment_parses() { - let source = "function f() { assembly { res := add(x, y) } }"; - let parsed = parse_supported_items(source); - assert!( - parsed.errors.is_empty(), - "top-level errors: {:?}", - parsed.errors - ); - let body_span = match parsed.output.as_slice() { - [ParsedTopItem::Function { body_span, .. }] => *body_span, - other => panic!("unexpected parse output: {other:?}"), - }; - let body = parse_body_statements(source, body_span); - assert!(body.errors.is_empty(), "body errors: {:?}", body.errors); - } - - #[test] - fn yul_call_expression_parses() { - let source = "add(x, y)"; - let (tokens, errors) = tokenize(source); - assert!(errors.is_empty(), "token errors: {:?}", errors); - assert!( - matches!( - tokens.first().map(|(tok, _)| tok), - Some(Token::Ident(name)) if *name == "add" - ), - "unexpected first token: {:?}", - tokens.first().map(|(tok, _)| tok) - ); - let stream = chumsky::input::Stream::from_iter(tokens) - .map((0..source.len()).into(), |(tok, span): (_, _)| (tok, span)); - let (output, parse_errors) = parsed_yul_expr_parser().parse(stream).into_output_errors(); - assert!( - parse_errors.is_empty(), - "parse errors: {:?}", - parse_errors - .into_iter() - .map(parse_error_from_rich) - .collect::>() - ); - assert!(output.is_some(), "expected parsed output"); - } -} diff --git a/crates/parser/src/parse/common.rs b/crates/parser/src/parse/common.rs new file mode 100644 index 00000000..5cd469d1 --- /dev/null +++ b/crates/parser/src/parse/common.rs @@ -0,0 +1,145 @@ +use chumsky::{input::ValueInput, prelude::*}; + +use crate::{lexer::Token, types::*}; + +pub(super) fn ident_parser<'src, I>() -> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::Ident(name) => name, + // `from` is contextual: it separates a selective/namespace import, + // but remains available as an ordinary name elsewhere (for example + // `Generic.from`). + Token::From => "from", + Token::True => "true", + Token::False => "false", + Token::Fallback => "fallback", + } + .validate(|name, e, emitter| { + if matches!(name, "true" | "false" | "fallback") { + emitter.emit(Rich::custom( + e.span(), + format!("`{name}` is reserved and cannot be used as an identifier"), + )); + } + if name.contains('-') { + emitter.emit(Rich::custom( + e.span(), + format!("identifier `{name}` cannot contain hyphens"), + )); + } + (name, e.span()) + }) +} + +pub(super) fn bool_ident_parser<'src, I>() -> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::True => "true", + Token::False => "false", + } + .map_with(|name, e| (name, e.span())) +} + +pub(super) fn non_comptime_param_name_parser<'src, I>() +-> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + ident_parser().validate(|name, _, emitter| { + if name.0 == "comptime" { + emitter.emit(Rich::custom( + name.1, + "`comptime` is a parameter modifier; expected parameter name", + )); + } + name + }) +} + +pub(super) fn qualified_ident_parser<'src, I>() +-> impl Parser<'src, I, Vec>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + ident_parser() + .separated_by(just(Token::Dot)) + .at_least(1) + .collect::>() +} + +pub(super) fn comptime_kw_parser<'src, I>() -> impl Parser<'src, I, LexSpan, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Comptime).map_with(|_, e| e.span()) +} + +fn top_level_item_start_token_parser<'src, I>() -> impl Parser<'src, I, (), ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::Import | Token::Export | Token::Pragma | Token::Type | Token::Alias + | Token::Enum | Token::Struct | Token::Trait | Token::Impl | Token::Contract + | Token::Interface | Token::Library | Token::Function | Token::Constructor + | Token::Fallback | Token::Default => (), + } +} + +pub(super) fn top_level_semicolon_parser<'src, I>( + context: &'static str, +) -> impl Parser<'src, I, (), ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Semi) + .ignored() + .or(top_level_item_start_token_parser() + .validate(move |_, e, emitter| { + emitter.emit(Rich::custom( + e.span(), + format!("{context} requires trailing `;`"), + )); + }) + .rewind()) +} + +pub(super) fn operator_part_parser<'src, I>() -> impl Parser<'src, I, &'static str, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::ColonEq => ":=", + Token::Arrow => "->", + Token::FatArrow => "=>", + Token::EqEq => "==", + Token::NotEq => "!=", + Token::GreaterEq => ">=", + Token::LessEq => "<=", + Token::AndAnd => "&&", + Token::OrOr => "||", + Token::PlusEq => "+=", + Token::MinusEq => "-=", + Token::CaretEq => "^=", + Token::AmpEq => "&=", + Token::PipeEq => "|=", + Token::PercentEq => "%=", + Token::Plus => "+", + Token::Minus => "-", + Token::Star => "*", + Token::Slash => "/", + Token::Percent => "%", + Token::Bang => "!", + Token::Less => "<", + Token::Greater => ">", + Token::Eq => "=", + Token::Pipe => "|", + Token::Amp => "&", + Token::Caret => "^", + Token::Colon => ":", + } +} diff --git a/crates/parser/src/parse/errors.rs b/crates/parser/src/parse/errors.rs new file mode 100644 index 00000000..ad2dec07 --- /dev/null +++ b/crates/parser/src/parse/errors.rs @@ -0,0 +1,370 @@ +use chumsky::prelude::*; + +use crate::{ + lexer::{LexError, Token}, + types::*, +}; + +pub(super) fn lex_error( + source: &str, + start: usize, + end: usize, + span: LexSpan, + error: LexError, +) -> ParsedError { + match error { + LexError::Invalid => invalid_token_error(source, start, end, span), + LexError::UnterminatedBlockComment => ParsedError::new(span, "unterminated block comment") + .with_label("comment starts here") + .with_note("add `*/` before the end of file"), + LexError::InvalidStringEscape => { + ParsedError::new(span, invalid_string_escape_message(source, start, end)) + .with_label("invalid escape sequence") + } + } +} + +fn invalid_token_error(source: &str, start: usize, end: usize, span: LexSpan) -> ParsedError { + let snippet = source.get(start..end).unwrap_or(""); + if snippet.is_empty() { + ParsedError::new(span, "invalid token").with_label("invalid token") + } else if snippet.starts_with('"') && !string_literal_is_terminated(snippet) { + ParsedError::new(span, "unterminated string literal") + .with_label("string literal starts here") + .with_note("add a closing `\"` before the end of file") + } else { + ParsedError::new(span, format!("invalid token `{snippet}`")).with_label("invalid token") + } +} + +fn string_literal_is_terminated(snippet: &str) -> bool { + let mut escaped = false; + for ch in snippet.chars().skip(1) { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == '"' { + return true; + } + } + false +} + +fn invalid_string_escape_message(source: &str, start: usize, end: usize) -> String { + let snippet = source.get(start..end).unwrap_or(""); + let mut chars = snippet.chars(); + chars.next(); + while let Some(ch) = chars.next() { + if ch == '"' { + break; + } + if ch == '\\' + && let Some(escaped) = chars.next() + && !matches!(escaped, 'n' | 't' | '"' | '\\') + { + return format!("invalid string escape `\\{escaped}`"); + } + } + "invalid string escape".to_owned() +} + +fn token_spelling(token: &Token<'_>) -> &'static str { + match token { + Token::Contract => "contract", + Token::Interface => "interface", + Token::Library => "library", + Token::Import => "import", + Token::From => "from", + Token::Export => "export", + Token::As => "as", + Token::Let => "let", + Token::Comptime => "comptime", + Token::Enum => "enum", + Token::Struct => "struct", + Token::Trait => "trait", + Token::Impl => "impl", + Token::Alias => "alias", + Token::Is => "is", + Token::Where => "where", + Token::Returns => "returns", + Token::If => "if", + Token::Else => "else", + Token::For => "for", + Token::While => "while", + Token::Unchecked => "unchecked", + Token::Switch => "switch", + Token::Type => "type", + Token::Case => "case", + Token::Default => "default", + Token::Match => "match", + Token::Public => "public", + Token::External => "external", + Token::Internal => "internal", + Token::Private => "private", + Token::Pure => "pure", + Token::View => "view", + Token::Payable => "payable", + Token::Function => "function", + Token::Constructor => "constructor", + Token::Fallback => "fallback", + Token::Return => "return", + Token::Revert => "revert", + Token::Leave => "leave", + Token::Continue => "continue", + Token::Break => "break", + Token::Lam => "lam", + Token::Assembly => "assembly", + Token::Pragma => "pragma", + Token::True => "true", + Token::False => "false", + Token::ColonEq => ":=", + Token::Arrow => "->", + Token::FatArrow => "=>", + Token::EqEq => "==", + Token::NotEq => "!=", + Token::GreaterEq => ">=", + Token::LessEq => "<=", + Token::AndAnd => "&&", + Token::OrOr => "||", + Token::PlusEq => "+=", + Token::MinusEq => "-=", + Token::CaretEq => "^=", + Token::AmpEq => "&=", + Token::PipeEq => "|=", + Token::PercentEq => "%=", + Token::Plus => "+", + Token::Minus => "-", + Token::Star => "*", + Token::Slash => "/", + Token::Percent => "%", + Token::Bang => "!", + Token::Tilde => "~", + Token::Less => "<", + Token::Greater => ">", + Token::Eq => "=", + Token::Pipe => "|", + Token::Amp => "&", + Token::Caret => "^", + Token::At => "@", + Token::Question => "?", + Token::Dot => ".", + Token::Colon => ":", + Token::Semi => ";", + Token::Comma => ",", + Token::LParen => "(", + Token::RParen => ")", + Token::LBrace => "{", + Token::RBrace => "}", + Token::LBracket => "[", + Token::RBracket => "]", + Token::Underscore => "_", + Token::LineComment => "//", + Token::BlockComment => "/* */", + Token::Ident(_) => "identifier", + Token::HexLit(_) => "hex literal", + Token::Number(_) => "number literal", + Token::String(_) => "string literal", + } +} + +pub(super) fn token_found_description(token: &Token<'_>) -> String { + match token { + Token::Ident(name) => format!("identifier `{name}`"), + Token::Number(value) => format!("number literal `{value}`"), + Token::HexLit(value) => format!("hex literal `{value}`"), + Token::String(value) => format!("string literal {value}"), + _ => format!("`{}`", token_spelling(token)), + } +} + +fn token_expected_description(token: &Token<'_>) -> String { + match token { + Token::Ident(_) => "identifier".to_owned(), + Token::Number(_) => "number literal".to_owned(), + Token::HexLit(_) => "hex literal".to_owned(), + Token::String(_) => "string literal".to_owned(), + _ => format!("`{}`", token_spelling(token)), + } +} + +fn expected_pattern_description(pattern: &chumsky::error::RichPattern<'_, Token<'_>>) -> String { + match pattern { + chumsky::error::RichPattern::Token(token) => token_expected_description(token), + chumsky::error::RichPattern::Label(label) => label.to_string(), + chumsky::error::RichPattern::Identifier(name) => { + format!("identifier `{}`", name.trim_matches('"')) + } + chumsky::error::RichPattern::Any => "token".to_owned(), + chumsky::error::RichPattern::SomethingElse => "different token".to_owned(), + chumsky::error::RichPattern::EndOfInput => "end of input".to_owned(), + _ => "token".to_owned(), + } +} + +fn format_expected_list(expected: &[chumsky::error::RichPattern<'_, Token<'_>>]) -> String { + let mut items = expected + .iter() + .map(expected_pattern_description) + .collect::>(); + let has_specific = items + .iter() + .any(|item| item != "token" && item != "different token"); + if has_specific { + items.retain(|item| item != "token" && item != "different token"); + } + items.sort_unstable(); + items.dedup(); + + match items.as_slice() { + [] => "something else".to_owned(), + [single] => single.clone(), + _ => { + let last = items.pop().expect("non-empty list has a last element"); + format!("{}, or {last}", items.join(", ")) + } + } +} + +fn expected_found_message( + _expected: &[chumsky::error::RichPattern<'_, Token<'_>>], + found: Option<&Token<'_>>, +) -> String { + match found { + Some(found) => format!("parse error: unexpected {}", token_found_description(found)), + None => "parse error: unexpected end of input".to_owned(), + } +} + +fn parser_context(error: &Rich<'_, Token<'_>, LexSpan>) -> Option { + error.contexts().find_map(|(pattern, _)| match pattern { + chumsky::error::RichPattern::Label(label) => Some(label.to_string()), + _ => None, + }) +} + +fn expected_note( + expected: &[chumsky::error::RichPattern<'_, Token<'_>>], + context: Option<&str>, + found: Option<&Token<'_>>, +) -> Option { + let mut expected_text = format_expected_list(expected); + if matches!(expected_text.as_str(), "something else" | "different token") + && matches!( + context, + Some( + "contract declaration" + | "function signature" + | "function parameter" + | "pragma declaration" + ) + ) + { + expected_text = "identifier".to_owned(); + } + if matches!(context, Some("import declaration")) + && matches!(found, Some(Token::Semi)) + && expected_text == "`{`" + { + expected_text = "import selector after `.`".to_owned(); + } + + if matches!(expected_text.as_str(), "something else" | "different token") { + None + } else { + Some(format!("expecting {expected_text}")) + } +} + +fn keyword_identifier_note( + context: Option<&str>, + found: Option<&Token<'_>>, +) -> Option<&'static str> { + let found = found?; + if !matches!( + context, + Some("function signature" | "contract declaration" | "function parameter") + ) || !is_reserved_keyword(found) + { + return None; + } + Some("keywords cannot be used as identifiers; choose a different name") +} + +fn is_reserved_keyword(token: &Token<'_>) -> bool { + matches!( + token, + Token::Contract + | Token::Interface + | Token::Library + | Token::Import + | Token::Export + | Token::As + | Token::Let + | Token::Comptime + | Token::Enum + | Token::Struct + | Token::Trait + | Token::Impl + | Token::Alias + | Token::Is + | Token::Where + | Token::Returns + | Token::If + | Token::Else + | Token::For + | Token::While + | Token::Unchecked + | Token::Switch + | Token::Type + | Token::Case + | Token::Default + | Token::Match + | Token::Public + | Token::External + | Token::Internal + | Token::Private + | Token::Pure + | Token::View + | Token::Payable + | Token::Function + | Token::Constructor + | Token::Return + | Token::Revert + | Token::Leave + | Token::Continue + | Token::Break + | Token::Lam + | Token::Assembly + | Token::Pragma + | Token::True + | Token::False + | Token::Fallback + ) +} + +pub(super) fn parse_error_from_rich<'src>(error: Rich<'src, Token<'src>, LexSpan>) -> ParsedError { + let context = parser_context(&error); + let mut parsed = match error.reason() { + chumsky::error::RichReason::Custom(msg) => ParsedError::new(*error.span(), msg.clone()), + chumsky::error::RichReason::ExpectedFound { expected, found } => { + let found = found.as_deref(); + let mut parsed = + ParsedError::new(*error.span(), expected_found_message(expected, found)) + .with_label("unexpected token"); + if let Some(note) = expected_note(expected, context.as_deref(), found) { + parsed = parsed.with_note(note); + } + if let Some(note) = keyword_identifier_note(context.as_deref(), found) { + parsed = parsed.with_note(note); + } + parsed + } + }; + if let Some(ctx) = context + && matches!(parsed.label.as_deref(), None | Some("unexpected token")) + { + parsed = parsed.with_note(format!("while parsing {ctx}")); + } + parsed +} diff --git a/crates/parser/src/parse/expr_pat.rs b/crates/parser/src/parse/expr_pat.rs new file mode 100644 index 00000000..93e55086 --- /dev/null +++ b/crates/parser/src/parse/expr_pat.rs @@ -0,0 +1,527 @@ +use chumsky::{input::ValueInput, prelude::*}; +use hir::ast::function; + +use super::{ + common::*, + items::{body_span_parser, param_parser}, + recovery::trace_recovery, + types::type_parser, +}; +use crate::{lexer::Token, types::*}; + +#[derive(Debug, Clone)] +enum ParsedPostfixOp<'src> { + Index(ParsedExpr<'src>), + Call(Vec>), + Field(SpannedStr<'src>), +} + +fn parsed_lit_parser<'src, I>() -> impl Parser<'src, I, ParsedLitKind<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::Number(n) => ParsedLitKind::Number(n), + Token::HexLit(h) => ParsedLitKind::Hex(h), + Token::String(s) => ParsedLitKind::String(s), + } + .boxed() +} + +fn parsed_bin_op_expr<'src>( + lhs: ParsedExpr<'src>, + op: ParsedSpanned<'src, function::BinOp>, + rhs: ParsedExpr<'src>, + span: LexSpan, +) -> ParsedExpr<'src> { + ParsedExpr { + span, + kind: ParsedExprKind::BinOp { + lhs: Box::new(lhs), + op, + rhs: Box::new(rhs), + }, + } +} + +pub(super) fn parsed_expr_parser<'src, I>() +-> impl Parser<'src, I, ParsedExpr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + expr_pat_parsers().0 +} + +pub(super) fn parsed_pat_parser<'src, I>() -> impl Parser<'src, I, ParsedPat<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + expr_pat_parsers().1 +} + +fn expr_pat_parsers<'src, I>() -> ( + impl Parser<'src, I, ParsedExpr<'src>, ParserErr<'src>>, + impl Parser<'src, I, ParsedPat<'src>, ParserErr<'src>>, +) +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + // Expressions and patterns are mutually recursive: patterns can contain + // comptime expressions, while expressions contain match arms with patterns. + // `Recursive::declare` lets both parser handles exist before either grammar + // is defined. + let mut expr = Recursive::declare(); + let mut pat = Recursive::declare(); + + expr.define({ + let lambda_param = param_parser().boxed(); + + let lambda_params = lambda_param + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|params, e| (params, e.span())) + .boxed(); + + let lambda_return = type_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|elems, e| match <[_; 1]>::try_from(elems) { + Ok([ty]) => ty, + Err(elems) => ParsedTy { + span: e.span(), + kind: ParsedTyKind::Tuple { elems }, + }, + }) + .boxed(); + + let lambda_expr = just(Token::Lam) + .ignore_then(lambda_params) + .then(just(Token::Returns).ignore_then(lambda_return).or_not()) + .then(body_span_parser()) + .map_with(|(((params, params_span), ret), body_span), e| ParsedExpr { + span: e.span(), + kind: ParsedExprKind::Lambda { + params, + params_span, + ret, + body_span, + }, + }) + .boxed(); + + let boundary = choice(( + just(Token::Semi).ignored(), + just(Token::Comma).ignored(), + just(Token::RParen).ignored(), + just(Token::RBracket).ignored(), + just(Token::RBrace).ignored(), + just(Token::Else).ignored(), + just(Token::Question).ignored(), + just(Token::Colon).ignored(), + just(Token::FatArrow).ignored(), + just(Token::Pipe).ignored(), + )); + let atom_recovery = any() + .and_is(boundary.not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("expr_atom", span); + ParsedExpr { + span, + kind: ParsedExprKind::Error, + } + }); + + let tuple_or_paren_expr = expr + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|elems, e| match <[_; 1]>::try_from(elems) { + Ok([expr]) => expr, + Err(elems) => ParsedExpr { + span: e.span(), + kind: ParsedExprKind::Tuple(elems), + }, + }) + .boxed(); + + let proxy_expr = just(Token::At) + .map_with(|_, e| e.span()) + .then(type_parser()) + .map_with(|(at, ty), e| ParsedExpr { + span: e.span(), + kind: ParsedExprKind::Proxy { at, ty }, + }) + .boxed(); + + let bool_ident = bool_ident_parser() + .map(|ident| ParsedExpr { + span: ident.1, + kind: ParsedExprKind::Ident(ident), + }) + .boxed(); + + let atom = parsed_lit_parser() + .map_with(|lit, e| ParsedExpr { + span: e.span(), + kind: ParsedExprKind::Lit(lit), + }) + .or(bool_ident) + .or(ident_parser().map(|ident| ParsedExpr { + span: ident.1, + kind: ParsedExprKind::Ident(ident), + })) + .or(proxy_expr) + .or(tuple_or_paren_expr) + .or(lambda_expr) + .recover_with(via_parser(atom_recovery)) + .boxed(); + + let index_op = expr + .clone() + .delimited_by(just(Token::LBracket), just(Token::RBracket)) + .map(ParsedPostfixOp::Index); + let call_op = expr + .clone() + .separated_by(just(Token::Comma)) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map(ParsedPostfixOp::Call); + let field_op = just(Token::Dot) + .ignore_then(ident_parser()) + .map(ParsedPostfixOp::Field); + + let postfix = atom + .foldl_with( + index_op.or(call_op).or(field_op).repeated(), + |base, op, e| ParsedExpr { + span: e.span(), + kind: match op { + ParsedPostfixOp::Index(index) => ParsedExprKind::Index { + base: Box::new(base), + index: Box::new(index), + }, + ParsedPostfixOp::Call(args) => ParsedExprKind::Call { + callee: Box::new(base), + args, + }, + ParsedPostfixOp::Field(field) => ParsedExprKind::Field { + base: Box::new(base), + field, + }, + }, + }, + ) + .boxed(); + + let unary_op = just(Token::Bang) + .to(function::UnOp::Not) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let unary = unary_op + .repeated() + .foldr_with(postfix, |op, expr, e| ParsedExpr { + span: e.span(), + kind: ParsedExprKind::UnaryOp { + op, + expr: Box::new(expr), + }, + }) + .boxed(); + + let conversion = unary + .clone() + .foldl_with( + just(Token::As).ignore_then(type_parser()).repeated(), + |expr, ty, e| ParsedExpr { + span: e.span(), + kind: ParsedExprKind::Conversion { + expr: Box::new(expr), + ty, + }, + }, + ) + .boxed(); + + let pow_op = just(Token::Star) + .then_ignore(just(Token::Star)) + .to(function::BinOp::Pow) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let pow = recursive(|pow| { + conversion + .clone() + .then(pow_op.then(pow).or_not()) + .map_with(|(lhs, rhs), e| match rhs { + Some((op, rhs)) => parsed_bin_op_expr(lhs, op, rhs, e.span()), + None => lhs, + }) + }) + .boxed(); + + let mul_op = select! { + Token::Star => function::BinOp::Mul, + Token::Slash => function::BinOp::Div, + Token::Percent => function::BinOp::Mod, + } + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let mul = pow + .clone() + .foldl_with(mul_op.then(pow).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let add_op = select! { + Token::Plus => function::BinOp::Add, + Token::Minus => function::BinOp::Sub, + } + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let add = mul + .clone() + .foldl_with(add_op.then(mul).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let shift_op = choice(( + just(Token::Less) + .then_ignore(just(Token::Less)) + .to(function::BinOp::Shl), + just(Token::Greater) + .then_ignore(just(Token::Greater)) + .to(function::BinOp::Shr), + )) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let shift = add + .clone() + .foldl_with(shift_op.then(add).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let bit_and_op = just(Token::Amp) + .to(function::BinOp::BitAnd) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let bit_and = shift + .clone() + .foldl_with(bit_and_op.then(shift).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let bit_xor_op = just(Token::Caret) + .to(function::BinOp::BitXor) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let bit_xor = bit_and + .clone() + .foldl_with(bit_xor_op.then(bit_and).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let bit_or_op = just(Token::Pipe) + .to(function::BinOp::BitOr) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let bit_or = bit_xor + .clone() + .foldl_with(bit_or_op.then(bit_xor).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let rel_op = select! { + Token::Less => function::BinOp::Lt, + Token::Greater => function::BinOp::Gt, + Token::LessEq => function::BinOp::LtEq, + Token::GreaterEq => function::BinOp::GtEq, + } + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let rel = bit_or + .clone() + .then(rel_op.then(bit_or).or_not()) + .map_with(|(lhs, rhs), e| match rhs { + Some((op, rhs)) => parsed_bin_op_expr(lhs, op, rhs, e.span()), + None => lhs, + }) + .boxed(); + + let eq_op = select! { + Token::EqEq => function::BinOp::Eq, + Token::NotEq => function::BinOp::NotEq, + } + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let eq = rel + .clone() + .then(eq_op.then(rel).or_not()) + .map_with(|(lhs, rhs), e| match rhs { + Some((op, rhs)) => parsed_bin_op_expr(lhs, op, rhs, e.span()), + None => lhs, + }) + .boxed(); + + let and_op = just(Token::AndAnd) + .to(function::BinOp::And) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let and = eq + .clone() + .foldl_with(and_op.then(eq).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let or_op = just(Token::OrOr) + .to(function::BinOp::Or) + .map_with(|op, e| ParsedSpanned::new(op, e.span())); + let or = and + .clone() + .foldl_with(or_op.then(and).repeated(), |lhs, (op, rhs), e| { + parsed_bin_op_expr(lhs, op, rhs, e.span()) + }) + .boxed(); + + let ternary = recursive(|ternary| { + or.clone() + .then( + just(Token::Question) + .ignore_then(ternary.clone()) + .then_ignore(just(Token::Colon)) + .then(ternary) + .or_not(), + ) + .map_with(|(cond, arms), e| match arms { + Some((then_expr, else_expr)) => ParsedExpr { + span: e.span(), + kind: ParsedExprKind::If { + cond: Box::new(cond), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + }, + }, + None => cond, + }) + }) + .boxed(); + + ternary + }); + + pat.define({ + let wildcard = just(Token::Underscore) + .map_with(|_, e| ParsedPat { + span: e.span(), + kind: ParsedPatKind::Wildcard, + }) + .boxed(); + + let lit_pat = parsed_lit_parser() + .map_with(|lit, e| ParsedPat { + span: e.span(), + kind: ParsedPatKind::Lit(lit), + }) + .boxed(); + + let tuple_or_paren_pat = pat + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|pats, e| match <[_; 1]>::try_from(pats) { + Ok([pat]) => pat, + Err(pats) => ParsedPat { + span: e.span(), + kind: ParsedPatKind::Tuple(pats), + }, + }) + .boxed(); + + let ctor_args = pat + .clone() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .boxed(); + + let comptime_pat = comptime_kw_parser() + .then(expr.clone()) + .map_with(|(kw, expr), e| ParsedPat { + span: e.span(), + kind: ParsedPatKind::ComptimeLabel { kw, expr }, + }) + .boxed(); + + let bool_pat = bool_ident_parser() + .map(|name| ParsedPat { + span: name.1, + kind: ParsedPatKind::Var(name), + }) + .boxed(); + + let ctor_or_var = qualified_ident_parser() + .then(ctor_args) + .map_with(|(mut path, args), e| { + let name = path.pop().expect("qualified path has at least one segment"); + let is_unqualified_var = path.is_empty() + && args.is_none() + && name + .0 + .chars() + .next() + .is_none_or(|first| first.is_lowercase()); + ParsedPat { + span: e.span(), + kind: if is_unqualified_var { + ParsedPatKind::Var(name) + } else { + ParsedPatKind::Ctor { + leading_dot: None, + qualifiers: path, + name, + args: args.unwrap_or_default(), + } + }, + } + }) + .boxed(); + + let boundary = just(Token::Comma) + .or(just(Token::RParen)) + .or(just(Token::LBrace)) + .or(just(Token::Case)) + .or(just(Token::Default)) + .or(just(Token::FatArrow)) + .or(just(Token::Pipe)) + .or(just(Token::RBrace)); + let recovery = any() + .and_is(boundary.not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("pattern", span); + ParsedPat { + span, + kind: ParsedPatKind::Error, + } + }); + + wildcard + .or(lit_pat) + .or(tuple_or_paren_pat) + .or(comptime_pat) + .or(bool_pat) + .or(ctor_or_var) + .recover_with(via_parser(recovery)) + }); + + (expr.labelled("expression"), pat.labelled("pattern")) +} diff --git a/crates/parser/src/parse/imports.rs b/crates/parser/src/parse/imports.rs new file mode 100644 index 00000000..c0b0b4fa --- /dev/null +++ b/crates/parser/src/parse/imports.rs @@ -0,0 +1,319 @@ +use chumsky::{input::ValueInput, prelude::*}; + +use super::common::*; +use crate::{lexer::Token, types::*}; + +fn import_name_parser<'src, I>() -> impl Parser<'src, I, ParsedImportName, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let ident = ident_parser().map(|(name, span)| ParsedImportName { + name: name.to_owned(), + span, + is_operator: false, + }); + + let operator = operator_part_parser() + .repeated() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|parts, e| ParsedImportName { + name: parts.concat(), + span: e.span(), + is_operator: true, + }); + + choice((operator, ident)) + .labelled("selector name") + .as_context() +} + +fn constructor_selector_parser<'src, I>() +-> impl Parser<'src, I, ParsedConstructorSelector<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let names = ident_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>() + .map(ParsedConstructorSelector::Named); + let wildcard = just(Token::Star).to(ParsedConstructorSelector::All); + + choice((wildcard, names)) + .delimited_by(just(Token::LParen), just(Token::RParen)) + .labelled("constructor selector") + .as_context() +} + +fn export_wildcard_parser<'src, I>() -> impl Parser<'src, I, ParsedExportName<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Star).map_with(|_, e| ParsedExportName { + name: ParsedImportName { + name: "*".to_owned(), + span: e.span(), + is_operator: false, + }, + constructors: None, + }) +} + +fn export_name_parser<'src, I>() -> impl Parser<'src, I, ParsedExportName<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let ident = ident_parser() + .then(constructor_selector_parser().or_not()) + .map(|((name, span), constructors)| ParsedExportName { + name: ParsedImportName { + name: name.to_owned(), + span, + is_operator: false, + }, + constructors, + }); + let operator = import_name_parser() + .filter(|name| name.is_operator) + .map(|name| ParsedExportName { + name, + constructors: None, + }); + + choice((export_wildcard_parser(), operator, ident)) + .labelled("export name") + .as_context() +} + +pub(super) fn import_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let module_path = just(Token::At) + .map_with(|_, e| e.span()) + .or_not() + .then( + ident_parser() + .separated_by(just(Token::Dot)) + .at_least(1) + .collect::>(), + ) + .boxed(); + + let selected_item = ident_parser() + .then(just(Token::As).ignore_then(ident_parser()).or_not()) + .map(|((name, span), alias)| ParsedSelectedName { + name: ParsedImportName { + name: name.to_owned(), + span, + is_operator: false, + }, + alias, + constructors: None, + }); + let named_selector = selected_item + .separated_by(just(Token::Comma)) + .at_least(1) + .allow_trailing() + .collect::>() + .map(ParsedImportSelector::Names) + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .boxed(); + + let selective = just(Token::Import) + .ignore_then(named_selector) + .then_ignore(just(Token::From)) + .then(module_path.clone()) + .then_ignore(top_level_semicolon_parser("import declaration")) + .map_with(|(selector, (external, path)), e| ParsedTopItem::Import { + span: e.span(), + leading_comments: Vec::new(), + external, + path, + alias: None, + selector: Some(selector), + hiding: Vec::new(), + }) + .boxed(); + + let namespace_alias = just(Token::Import) + .ignore_then(just(Token::Star)) + .ignore_then(just(Token::As)) + .ignore_then(ident_parser()) + .then_ignore(just(Token::From)) + .then(module_path.clone()) + .then_ignore(top_level_semicolon_parser("import declaration")) + .map_with(|(alias, (external, path)), e| ParsedTopItem::Import { + span: e.span(), + leading_comments: Vec::new(), + external, + path, + alias: Some(alias), + selector: None, + hiding: Vec::new(), + }) + .boxed(); + + let plain = just(Token::Import) + .ignore_then(module_path) + .then_ignore(top_level_semicolon_parser("import declaration")) + .map_with(|(external, path), e| ParsedTopItem::Import { + span: e.span(), + leading_comments: Vec::new(), + external, + path, + alias: None, + // Like Solidity's bare import, `import M;` brings M's public + // surface into the current module. Namespace imports use the + // explicit `import * as name from M;` spelling above. + selector: Some(ParsedImportSelector::Wildcard), + hiding: Vec::new(), + }) + .boxed(); + + choice((namespace_alias, selective, plain)) + .labelled("import declaration") + .as_context() + .boxed() +} + +/// Parses the legacy export surface as a temporary compatibility extension. +/// +/// `new_syntax.md` intentionally leaves Core's public-interface and re-export +/// policy unspecified. Keeping this parser is not an endorsement of any of +/// these spellings as canonical syntax; it only avoids coupling the import +/// migration to that still-open design decision. +pub(super) fn export_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let path = ident_parser() + .separated_by(just(Token::Dot)) + .at_least(1) + .collect::>() + .boxed(); + + let module_wildcard = path + .clone() + .then_ignore(just(Token::Dot)) + .then_ignore(just(Token::Star)) + .map_with(|path, e| ParsedImportName { + name: path + .into_iter() + .map(|(name, _)| name) + .collect::>() + .join(".") + + ".*", + span: e.span(), + is_operator: false, + }) + .map(|name| ParsedExportName { + name, + constructors: None, + }); + let export_item = choice((module_wildcard, export_name_parser())); + let export_list_items = export_item + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .boxed(); + let export_selector_items = choice(( + export_wildcard_parser().map(|name| vec![name]), + export_name_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + )) + .boxed(); + + let export_list = just(Token::Export) + .ignore_then(export_list_items) + .then_ignore(just(Token::Semi)) + .map_with(|names, e| ParsedTopItem::Export { + span: e.span(), + leading_comments: Vec::new(), + kind: ParsedExportKind::List(names), + }); + let items_from = just(Token::Export) + .ignore_then(path.clone()) + .then_ignore(just(Token::Dot)) + .then(export_selector_items) + .then_ignore(just(Token::Semi)) + .map_with(|(path, names), e| ParsedTopItem::Export { + span: e.span(), + leading_comments: Vec::new(), + kind: ParsedExportKind::ItemsFrom(path, names), + }); + let module_as = just(Token::Export) + .ignore_then(path.clone()) + .then_ignore(just(Token::As)) + .then(ident_parser()) + .then_ignore(just(Token::Semi)) + .map_with(|(path, alias), e| ParsedTopItem::Export { + span: e.span(), + leading_comments: Vec::new(), + kind: ParsedExportKind::ModuleAs(path, alias), + }); + let module = just(Token::Export) + .ignore_then(path) + .then_ignore(just(Token::Semi)) + .map_with(|path, e| ParsedTopItem::Export { + span: e.span(), + leading_comments: Vec::new(), + kind: ParsedExportKind::Module(path), + }); + + choice((export_list, items_from, module_as, module)) + .labelled("export declaration") + .as_context() + .boxed() +} + +pub(super) fn pragma_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let solcore_namespace = + select! { Token::Ident(name) if name == "solcore" => () }.labelled("solcore"); + let solcore_items = ident_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(); + + let solcore = solcore_namespace + .ignore_then(ident_parser()) + .then(solcore_items) + .then_ignore(just(Token::Semi)) + .boxed(); + + // Solidity pragmas are accepted for source-level interoperability, but + // their version/configuration payload is intentionally opaque to Core. + // Preserve only the pragma family in HIR and skip tokens through `;`. + let opaque_name = select! { + Token::Ident(name) if matches!(name, "solidity" | "abicoder") => name, + } + .map_with(|name, e| (name, e.span())) + .labelled("solidity or abicoder"); + let opaque_payload = any().and_is(just(Token::Semi).not()).repeated().ignored(); + let opaque = opaque_name + .then_ignore(opaque_payload) + .then_ignore(just(Token::Semi)) + .map(|name| (name, Vec::new())) + .boxed(); + + just(Token::Pragma) + .ignore_then(choice((solcore, opaque))) + .map_with(|(name, items), e| ParsedTopItem::Pragma { + span: e.span(), + leading_comments: Vec::new(), + name, + items, + }) + .labelled("pragma declaration") + .as_context() + .boxed() +} diff --git a/crates/parser/src/parse/items.rs b/crates/parser/src/parse/items.rs new file mode 100644 index 00000000..10aa4aaf --- /dev/null +++ b/crates/parser/src/parse/items.rs @@ -0,0 +1,1110 @@ +use chumsky::{input::ValueInput, prelude::*}; +use hir::ast::{ + function::{FunctionMutability, FunctionVisibility}, + item::{ContractKind, FuncKind}, +}; + +use super::{ + common::*, + expr_pat::parsed_expr_parser, + imports::{export_parser, import_parser, pragma_parser}, + recovery::trace_recovery, + types::{trait_ref_parser, type_param_list_parser, type_parser, where_clause_parser}, +}; +use crate::{lexer::Token, types::*}; + +pub(super) fn param_parser<'src, I>() -> impl Parser<'src, I, ParsedFuncParam<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let comptime_typed = comptime_kw_parser() + .then(ident_parser()) + .then_ignore(just(Token::Colon)) + .rewind() + .ignore_then(comptime_kw_parser()) + .then(ident_parser()) + .then_ignore(just(Token::Colon)) + .then(type_parser()) + .map(|((comptime, name), ty)| ParsedFuncParam::Typed { + comptime: Some(comptime), + name, + ty, + }) + .boxed(); + + let param_end = just(Token::Comma).or(just(Token::RParen)).ignored(); + let comptime_untyped = comptime_kw_parser() + .then(ident_parser()) + .then_ignore(param_end.rewind()) + .rewind() + .ignore_then(comptime_kw_parser()) + .then(ident_parser()) + .map(|(comptime, name)| ParsedFuncParam::Untyped { + comptime: Some(comptime), + name, + }) + .boxed(); + + let typed = non_comptime_param_name_parser() + .then_ignore(just(Token::Colon)) + .then(type_parser()) + .map(|(name, ty)| ParsedFuncParam::Typed { + comptime: None, + name, + ty, + }) + .boxed(); + + let untyped = non_comptime_param_name_parser() + .map(|name| ParsedFuncParam::Untyped { + comptime: None, + name, + }) + .boxed(); + + let recovery = any() + .and_is(just(Token::Comma).not()) + .and_is(just(Token::RParen).not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("function_param", span); + ParsedFuncParam::Error { span } + }); + + choice((comptime_typed, comptime_untyped, typed, untyped)) + .recover_with(via_parser(recovery)) + .labelled("function parameter") + .as_context() +} + +#[derive(Debug, Clone, Copy, Default)] +struct ParsedFuncModifiers { + visibility: Option<(FunctionVisibility, LexSpan)>, + mutability: Option<(FunctionMutability, LexSpan)>, +} + +#[derive(Debug, Clone, Copy)] +enum ParsedFuncModifier { + Visibility(FunctionVisibility, LexSpan), + Mutability(FunctionMutability, LexSpan), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FunctionContext { + Module, + Contract, +} + +fn function_modifiers_parser<'src, I>( + _context: FunctionContext, +) -> impl Parser<'src, I, ParsedFuncModifiers, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let modifier = choice(( + just(Token::Public) + .map_with(|_, e| ParsedFuncModifier::Visibility(FunctionVisibility::Public, e.span())) + .boxed(), + just(Token::External) + .map_with(|_, e| ParsedFuncModifier::Visibility(FunctionVisibility::External, e.span())) + .boxed(), + just(Token::Internal) + .map_with(|_, e| ParsedFuncModifier::Visibility(FunctionVisibility::Internal, e.span())) + .boxed(), + just(Token::Private) + .map_with(|_, e| ParsedFuncModifier::Visibility(FunctionVisibility::Private, e.span())) + .boxed(), + just(Token::Payable) + .map_with(|_, e| ParsedFuncModifier::Mutability(FunctionMutability::Payable, e.span())) + .boxed(), + just(Token::Pure) + .map_with(|_, e| ParsedFuncModifier::Mutability(FunctionMutability::Pure, e.span())) + .boxed(), + just(Token::View) + .map_with(|_, e| ParsedFuncModifier::Mutability(FunctionMutability::View, e.span())) + .boxed(), + )); + + modifier + .repeated() + .collect::>() + .validate(move |modifiers, _, emitter| { + let mut parsed = ParsedFuncModifiers::default(); + for modifier in modifiers { + match modifier { + ParsedFuncModifier::Visibility(kind, span) => { + if let Some((previous, _)) = parsed.visibility { + let conflict = if previous == kind { + "duplicate" + } else { + "conflicting" + }; + emitter.emit(Rich::custom( + span, + format!( + "{conflict} function visibility `{}`; `{}` was already specified", + kind.keyword(), + previous.keyword() + ), + )); + } else { + parsed.visibility = Some((kind, span)); + } + } + ParsedFuncModifier::Mutability(kind, span) => { + if let Some((previous, _)) = parsed.mutability { + let conflict = if previous == kind { + "duplicate" + } else { + "conflicting" + }; + emitter.emit(Rich::custom( + span, + format!( + "{conflict} function mutability `{}`; `{}` was already specified", + kind.keyword(), + previous.keyword() + ), + )); + } else { + parsed.mutability = Some((kind, span)); + } + } + } + } + parsed + }) +} + +fn implicit_public_modifiers_parser<'src, I>( + context: FunctionContext, + decl_name: &'static str, + allow_external: bool, +) -> impl Parser<'src, I, ParsedFuncModifiers, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + function_modifiers_parser(context).validate(move |modifiers, _, emitter| { + if let Some((visibility, span)) = modifiers.visibility { + match visibility { + FunctionVisibility::Public if allow_external => emitter.emit(Rich::custom( + span, + format!("`public` is not allowed on {decl_name}; use `external`"), + )), + FunctionVisibility::Public => emitter.emit(Rich::custom( + span, + format!("{decl_name} is implicitly public; remove the visibility keyword"), + )), + FunctionVisibility::External if allow_external => {} + FunctionVisibility::External + | FunctionVisibility::Internal + | FunctionVisibility::Private => emitter.emit(Rich::custom( + span, + format!("`{}` is not allowed on {decl_name}", visibility.keyword()), + )), + } + } + if let Some((mutability, span)) = modifiers.mutability + && mutability != FunctionMutability::Payable + { + emitter.emit(Rich::custom( + span, + format!("`{}` is not allowed on {decl_name}", mutability.keyword()), + )); + } + modifiers + }) +} + +fn return_type_parser<'src, I>() +-> impl Parser<'src, I, Option<(ParsedTy<'src>, Vec>>)>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let named = ident_parser() + .then_ignore(just(Token::Colon)) + .rewind() + .ignore_then(ident_parser()) + .then_ignore(just(Token::Colon)) + .then(type_parser()) + .map(|(name, ty)| (Some(name), ty)) + .boxed(); + let result = named.or(type_parser().map(|ty| (None, ty))).boxed(); + let results = result + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|results, e| { + let span = e.span(); + let (names, results): (Vec<_>, Vec<_>) = results.into_iter().unzip(); + let ty = match <[_; 1]>::try_from(results) { + Ok([result]) => result, + Err(elems) => ParsedTy { + span, + kind: ParsedTyKind::Tuple { elems }, + }, + }; + (ty, names) + }); + + just(Token::Returns).ignore_then(results).or_not() +} + +fn signature_parser<'src, I>( + context: FunctionContext, +) -> impl Parser<'src, I, ParsedFuncSig<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let params = param_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|params, e| (params, e.span())) + .boxed(); + + just(Token::Function) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then(params) + .then(function_modifiers_parser(context)) + .then(return_type_parser()) + .then(where_clause_parser()) + .map_with( + |(((((name, type_vars), (params, params_span)), modifiers), ret), preds), e| { + let (ret, ret_names) = match ret { + Some((ret, names)) => (Some(ret), names), + None => (None, Vec::new()), + }; + ParsedFuncSig { + span: e.span(), + type_vars, + preds, + visibility: modifiers.visibility, + mutability: modifiers.mutability, + name, + params, + params_span, + ret, + ret_names, + } + }, + ) + .labelled("function signature") + .as_context() + .boxed() +} + +pub(super) fn body_span_parser<'src, I>() -> impl Parser<'src, I, LexSpan, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let body_contents = recursive(|body_contents| { + let nested = body_contents + .clone() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .ignored(); + + choice(( + nested, + any() + .and_is(just(Token::LBrace).not()) + .and_is(just(Token::RBrace).not()) + .ignored(), + )) + .repeated() + .ignored() + }); + + just(Token::LBrace) + .ignore_then(body_contents) + .then_ignore(just(Token::RBrace)) + .map_with(|_, e| e.span()) +} + +fn function_def_parser<'src, I>( + context: FunctionContext, +) -> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + signature_parser(context) + .then(body_span_parser()) + .map_with(|(sig, body_span), e| ParsedFunctionDef { + span: e.span(), + kind: FuncKind::Function, + leading_comments: Vec::new(), + sig, + body_span: Some(body_span), + }) + .labelled("function definition") + .as_context() + .boxed() +} + +fn function_member_parser<'src, I>( + context: FunctionContext, +) -> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let body_or_semi = body_span_parser().map(Some).or(just(Token::Semi).to(None)); + signature_parser(context) + .then(body_or_semi) + .map_with(|(sig, body_span), e| ParsedFunctionDef { + span: e.span(), + kind: FuncKind::Function, + leading_comments: Vec::new(), + sig, + body_span, + }) + .labelled("contract function") + .as_context() + .boxed() +} + +fn constructor_def_parser<'src, I>( + context: FunctionContext, +) -> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let params = param_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|params, e| (params, e.span())) + .boxed(); + + just(Token::Constructor) + .map_with(|_, e| e.span()) + .then(params) + .then(implicit_public_modifiers_parser( + context, + "constructor", + false, + )) + .then(body_span_parser()) + .map_with( + |(((name_span, (params, params_span)), modifiers), body_span), e| ParsedFunctionDef { + span: e.span(), + kind: FuncKind::Constructor, + leading_comments: Vec::new(), + sig: ParsedFuncSig { + span: e.span(), + type_vars: Vec::new(), + preds: Vec::new(), + visibility: modifiers.visibility, + mutability: modifiers.mutability, + name: ("constructor", name_span), + params, + params_span, + ret: None, + ret_names: Vec::new(), + }, + body_span: Some(body_span), + }, + ) + .labelled("constructor definition") + .as_context() + .boxed() +} + +fn parsed_ty_is_unit(ty: &ParsedTy<'_>) -> bool { + match &ty.kind { + ParsedTyKind::Tuple { elems } if elems.is_empty() => true, + ParsedTyKind::Tuple { elems } if elems.len() == 1 => parsed_ty_is_unit(&elems[0]), + _ => false, + } +} + +fn fallback_def_parser<'src, I>( + context: FunctionContext, +) -> impl Parser<'src, I, ParsedFunctionDef<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let params = param_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|params, e| (params, e.span())) + .boxed(); + + just(Token::Fallback) + .map_with(|_, e| e.span()) + .then(params) + .validate(|value, _, emitter| { + if !value.1.0.is_empty() { + emitter.emit(Rich::custom( + value.1.1, + "fallback function must not declare input parameters", + )); + } + value + }) + .then(implicit_public_modifiers_parser(context, "fallback", true)) + .then(return_type_parser()) + .validate(|value, _, emitter| { + if let Some((ret_ty, _)) = &value.1 + && !parsed_ty_is_unit(ret_ty) + { + emitter.emit(Rich::custom( + ret_ty.span, + "fallback function must return unit (`()`)", + )); + } + value + }) + .then(body_span_parser()) + .map_with( + |((((name_span, (params, params_span)), modifiers), ret), body_span), e| { + let (ret, ret_names) = match ret { + Some((ret, names)) => (Some(ret), names), + None => (None, Vec::new()), + }; + ParsedFunctionDef { + span: e.span(), + kind: FuncKind::Fallback, + leading_comments: Vec::new(), + sig: ParsedFuncSig { + span: e.span(), + type_vars: Vec::new(), + preds: Vec::new(), + visibility: modifiers.visibility, + mutability: modifiers.mutability, + name: ("fallback", name_span), + params, + params_span, + ret, + ret_names, + }, + body_span: Some(body_span), + } + }, + ) + .labelled("fallback definition") + .as_context() + .boxed() +} + +fn function_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + function_def_parser(FunctionContext::Module) + .map(|def| ParsedTopItem::Function { + span: def.span, + leading_comments: def.leading_comments, + sig: def.sig, + body_span: def.body_span.expect("top-level functions require a body"), + }) + .labelled("function declaration") + .as_context() + .boxed() +} + +fn type_alias_payload_parser<'src, I>() -> impl Parser< + 'src, + I, + ( + ParsedTypeAliasKind, + SpannedStr<'src>, + Vec>, + ParsedTy<'src>, + ), + ParserErr<'src>, +> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let type_recovery = any() + .and_is(just(Token::Semi).not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("type_alias_type", span); + ParsedTy { + span, + kind: ParsedTyKind::Error, + } + }); + + let alias = just(Token::Alias) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then_ignore(just(Token::Eq)) + .map(|(name, params)| (ParsedTypeAliasKind::Transparent, name, params)); + let value_type = just(Token::Type) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then_ignore(just(Token::Is)) + .map(|(name, params)| (ParsedTypeAliasKind::ValueType, name, params)); + + choice((alias, value_type)) + .then(type_parser().recover_with(via_parser(type_recovery))) + .then_ignore(just(Token::Semi)) + .map(|((kind, name, ty_params), ty)| (kind, name, ty_params, ty)) +} + +fn type_alias_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + type_alias_payload_parser() + .map_with(|(kind, name, ty_params, ty), e| ParsedTopItem::TypeAlias { + span: e.span(), + leading_comments: Vec::new(), + kind, + name, + ty_params, + ty, + }) + .labelled("type declaration") + .as_context() + .boxed() +} + +fn enum_ctor_parser<'src, I>() -> impl Parser<'src, I, ParsedAdtCtor<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let fields = type_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .map(Option::unwrap_or_default); + + ident_parser() + .then(fields) + .map_with(|(name, fields), e| ParsedAdtCtor { + span: e.span(), + introducer: Some(name.1), + leading_comments: Vec::new(), + name, + fields, + field_names: None, + }) + .boxed() +} + +fn enum_payload_parser<'src, I>() -> impl Parser< + 'src, + I, + ( + SpannedStr<'src>, + Vec>, + Vec>, + ), + ParserErr<'src>, +> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let ctors = enum_ctor_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)); + + just(Token::Enum) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then(ctors) + .then_ignore(just(Token::Semi).or_not()) + .map(|((name, ty_params), ctors)| (name, ty_params, ctors)) +} + +fn struct_payload_parser<'src, I>() -> impl Parser< + 'src, + I, + ( + SpannedStr<'src>, + Vec>, + Vec>, + ), + ParserErr<'src>, +> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let field = ident_parser() + .then_ignore(just(Token::Colon)) + .then(type_parser()) + .then_ignore(just(Token::Semi)); + let fields = field + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)); + + just(Token::Struct) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then(fields) + .then_ignore(just(Token::Semi).or_not()) + .map_with(|((name, ty_params), fields), e| { + let (field_names, fields) = fields.into_iter().unzip(); + let ctor = ParsedAdtCtor { + span: e.span(), + introducer: Some(name.1), + leading_comments: Vec::new(), + name, + fields, + field_names: Some(field_names), + }; + (name, ty_params, vec![ctor]) + }) +} + +fn adt_payload_parser<'src, I>() -> impl Parser< + 'src, + I, + ( + SpannedStr<'src>, + Vec>, + Vec>, + ), + ParserErr<'src>, +> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + choice((enum_payload_parser(), struct_payload_parser())) +} + +fn adt_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + adt_payload_parser() + .map_with(|(name, ty_params, ctors), e| ParsedTopItem::Adt { + span: e.span(), + leading_comments: Vec::new(), + name, + ty_params, + ctors, + }) + .labelled("enum or struct declaration") + .as_context() + .boxed() +} + +fn method_sig_parser<'src, I>() -> impl Parser<'src, I, ParsedClassMethod<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + signature_parser(FunctionContext::Module) + .then_ignore(just(Token::Semi)) + .map(|sig| ParsedClassMethod { + leading_comments: Vec::new(), + sig, + }) + .boxed() +} + +fn named_ty(name: SpannedStr<'_>) -> ParsedTy<'_> { + ParsedTy { + span: name.1, + kind: ParsedTyKind::Named { + qualifiers: Vec::new(), + name, + args: Vec::new(), + args_span: None, + }, + } +} + +fn trait_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let methods = method_sig_parser() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .boxed(); + + just(Token::Trait) + .ignore_then(ident_parser()) + .then(type_param_list_parser()) + .then(where_clause_parser()) + .then(methods) + .validate(|value, e, emitter| { + if value.0.0.1.is_empty() { + emitter.emit(Rich::custom( + e.span(), + "a trait must declare at least one type parameter", + )); + } + value + }) + .map_with(|(((class, type_vars), super_preds), methods), e| { + let mut args = type_vars.iter().copied().map(named_ty).collect::>(); + let ty = if args.is_empty() { + ParsedTy { + span: class.1, + kind: ParsedTyKind::Error, + } + } else { + args.remove(0) + }; + let head = ParsedPred { + ty, + class, + args, + args_span: None, + }; + ParsedTopItem::Class { + span: e.span(), + leading_comments: Vec::new(), + type_vars, + super_preds, + head, + methods, + } + }) + .labelled("trait declaration") + .as_context() + .boxed() +} + +fn impl_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let default_kw = just(Token::Default).map_with(|_, e| e.span()).or_not(); + let methods = function_def_parser(FunctionContext::Module) + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .boxed(); + + default_kw + .then_ignore(just(Token::Impl)) + .then(type_param_list_parser()) + .then(trait_ref_parser()) + .then(where_clause_parser()) + .then(methods) + .validate(|value, e, emitter| { + if value.0.0.1.1.is_empty() { + emitter.emit(Rich::custom( + e.span(), + "an impl trait reference must have at least one type argument", + )); + } + value + }) + .map_with( + |((((default_kw, type_vars), (class, mut head_args, args_span)), preds), methods), + e| { + let ty = if head_args.is_empty() { + ParsedTy { + span: class.1, + kind: ParsedTyKind::Error, + } + } else { + head_args.remove(0) + }; + let head = ParsedPred { + ty, + class, + args: head_args, + args_span, + }; + ParsedTopItem::Instance { + span: e.span(), + leading_comments: Vec::new(), + type_vars, + preds, + default_kw, + head, + methods, + } + }, + ) + .labelled("impl declaration") + .as_context() + .boxed() +} + +fn field_def_parser<'src, I>() -> impl Parser<'src, I, ParsedFieldDef<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + ident_parser() + .then_ignore(just(Token::Colon)) + .rewind() + .ignore_then(ident_parser()) + .then_ignore(just(Token::Colon)) + .then(type_parser()) + .then(just(Token::Eq).ignore_then(parsed_expr_parser()).or_not()) + .then_ignore(just(Token::Semi)) + .map_with(|((name, ty), init), e| ParsedFieldDef { + span: e.span(), + leading_comments: Vec::new(), + name, + ty, + init, + }) + .labelled("contract field") + .as_context() + .boxed() +} + +#[derive(Debug, Clone)] +enum ParsedContractMember<'src> { + Field(ParsedFieldDef<'src>), + Item(ParsedContractItem<'src>), +} + +fn contract_item_parser<'src, I>() -> impl Parser<'src, I, ParsedContractItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let function = function_member_parser(FunctionContext::Contract) + .map(ParsedContractItem::Function) + .boxed(); + let constructor = constructor_def_parser(FunctionContext::Contract) + .map(ParsedContractItem::Function) + .boxed(); + let fallback = fallback_def_parser(FunctionContext::Contract) + .map(ParsedContractItem::Function) + .boxed(); + let type_alias = type_alias_payload_parser() + .map_with( + |(kind, name, ty_params, ty), e| ParsedContractItem::TypeAlias { + span: e.span(), + leading_comments: Vec::new(), + kind, + name, + ty_params, + ty, + }, + ) + .boxed(); + let adt = adt_payload_parser() + .map_with(|(name, ty_params, ctors), e| ParsedContractItem::Adt { + span: e.span(), + leading_comments: Vec::new(), + name, + ty_params, + ctors, + }) + .boxed(); + + let item_start = just(Token::Function) + .or(just(Token::Constructor)) + .or(just(Token::Fallback)) + .or(just(Token::Alias)) + .or(just(Token::Type)) + .or(just(Token::Enum)) + .or(just(Token::Struct)) + .or(just(Token::RBrace)); + let recovery = any() + .and_is(item_start.not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("contract_member", span); + ParsedContractItem::Error { + span, + leading_comments: Vec::new(), + } + }); + + choice((function, constructor, fallback, type_alias, adt)) + .recover_with(via_parser(recovery)) + .labelled("contract member") + .as_context() +} + +fn contract_member_parser<'src, I>() +-> impl Parser<'src, I, ParsedContractMember<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + field_def_parser() + .map(ParsedContractMember::Field) + .or(contract_item_parser().map(ParsedContractMember::Item)) + .boxed() +} + +fn contract_parser<'src, I>() -> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let shell = choice(( + just(Token::Contract).to(ContractKind::Contract), + just(Token::Interface).to(ContractKind::Interface), + just(Token::Library).to(ContractKind::Library), + )); + let members = contract_member_parser() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)); + + shell + .then(ident_parser()) + .then(type_param_list_parser()) + .then(members) + .validate(|value, _, emitter| { + let (((kind, _), _), members) = &value; + for member in members { + let invalid = match member { + ParsedContractMember::Field(field) if *kind != ContractKind::Contract => { + Some(( + field.span, + format!( + "{} declarations cannot contain storage fields", + kind.keyword() + ), + )) + } + ParsedContractMember::Item(ParsedContractItem::Function(function)) => { + match (*kind, function.kind, function.body_span.is_some()) { + ( + ContractKind::Interface, + FuncKind::Constructor | FuncKind::Fallback, + _, + ) => Some(( + function.span, + format!( + "interface declarations cannot contain {} functions", + match function.kind { + FuncKind::Constructor => "constructor", + FuncKind::Fallback => "fallback", + FuncKind::Function => unreachable!(), + } + ), + )), + ( + ContractKind::Library, + FuncKind::Constructor | FuncKind::Fallback, + _, + ) => Some(( + function.span, + format!( + "library declarations cannot contain {} functions", + match function.kind { + FuncKind::Constructor => "constructor", + FuncKind::Fallback => "fallback", + FuncKind::Function => unreachable!(), + } + ), + )), + (ContractKind::Interface, FuncKind::Function, true) => Some(( + function.span, + "interface functions must be prototypes ending in `;`".to_owned(), + )), + (ContractKind::Interface, FuncKind::Function, false) + if function.sig.visibility.map(|(kind, _)| kind) + != Some(FunctionVisibility::External) => + { + Some(( + function + .sig + .visibility + .map_or(function.sig.span, |(_, span)| span), + "interface functions must be declared `external`".to_owned(), + )) + } + ( + ContractKind::Contract | ContractKind::Library, + FuncKind::Function, + false, + ) => Some(( + function.span, + format!("{} functions must provide a body", kind.keyword()), + )), + _ => None, + } + } + _ => None, + }; + if let Some((span, message)) = invalid { + emitter.emit(Rich::custom(span, message)); + } + } + value + }) + .map_with(|(((kind, name), ty_params), members), e| { + let mut fields = Vec::new(); + let mut items = Vec::new(); + for member in members { + match member { + ParsedContractMember::Field(field) => fields.push(field), + ParsedContractMember::Item(item) => items.push(item), + } + } + ParsedTopItem::Contract { + span: e.span(), + leading_comments: Vec::new(), + kind, + name, + ty_params, + fields, + items, + } + }) + .labelled("contract, interface, or library declaration") + .as_context() + .boxed() +} + +pub(super) fn top_item_parser<'src, I>() +-> impl Parser<'src, I, ParsedTopItem<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let item_start = just(Token::Import) + .or(just(Token::Export)) + .or(just(Token::Pragma)) + .or(just(Token::Alias)) + .or(just(Token::Type)) + .or(just(Token::Enum)) + .or(just(Token::Struct)) + .or(just(Token::Trait)) + .or(just(Token::Impl)) + .or(just(Token::Contract)) + .or(just(Token::Interface)) + .or(just(Token::Library)) + .or(just(Token::Function)) + .or(just(Token::Default)); + let recovery = any() + .and_is(item_start.not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("top_level_item", span); + ParsedTopItem::Error { + span, + leading_comments: Vec::new(), + } + }); + + choice(( + import_parser(), + export_parser(), + pragma_parser(), + type_alias_parser(), + adt_parser(), + trait_parser(), + impl_parser(), + contract_parser(), + function_parser(), + )) + .recover_with(via_parser(recovery)) + .labelled("top-level item") + .as_context() +} diff --git a/crates/parser/src/parse/mod.rs b/crates/parser/src/parse/mod.rs new file mode 100644 index 00000000..646d406d --- /dev/null +++ b/crates/parser/src/parse/mod.rs @@ -0,0 +1,1051 @@ +//! Chumsky grammar for Solcore source syntax. +//! +//! The grammar produces lightweight parsed nodes with absolute lexical spans. +//! Bodies are first captured as brace spans and parsed separately during +//! lowering so function/lambda bodies can receive their own def anchors. Error +//! recovery nodes are produced here, but diagnostics are collected after the +//! parsed output is lowered to HIR spans. + +mod common; +mod errors; +mod expr_pat; +mod imports; +mod items; +mod recovery; +mod stmt; +mod tokenize; +mod types; +mod yul; + +use chumsky::prelude::*; +use errors::parse_error_from_rich; +use items::top_item_parser; +use recovery::{ + lex_error_suppresses_parse_error, refine_body_parse_error, span_contains, + suppress_body_cascades, top_level_recovery_message, trace_recovery, +}; +use stmt::parsed_stmt_parser; +use tokenize::{tokenize_with_base, tokenize_with_comments}; + +use crate::types::*; + +/// Maximum recursive token-grammar nesting accepted before parsing. +/// +/// This bounds stack use in lowering and later HIR consumers on every target, +/// including wasm workers whose stack cannot be enlarged at runtime. +pub(crate) const MAX_SYNTAX_NESTING: usize = 128; +// HIR lowering has substantially larger recursion frames than token parsing. +// Keep enough headroom for native default stacks and wasm's smaller shadow +// stack, and stop ternary parsing at the same boundary before Chumsky recurses. +pub(crate) const MAX_EXPRESSION_NESTING: usize = 32; + +/// Parses the top-level items currently supported by the front end. +/// +/// Invalid top-level spans are represented as `ParsedTopItem::Error` and also +/// converted into user-facing parse errors. The function never panics on +/// malformed source. +pub(crate) fn parse_supported_items<'src>(src: &'src str) -> ParseOutput> { + let (tokens, comments, mut errors) = tokenize_with_comments(src); + let token_count = tokens.len(); + let stream = chumsky::input::Stream::from_iter(tokens) + .map((0..src.len()).into(), |(tok, span): (_, _)| (tok, span)); + + let (output, parse_errors) = top_item_parser() + .repeated() + .collect::>() + .parse(stream) + .into_output_errors(); + + let mut output = output.unwrap_or_default(); + attach_leading_comments(src, &comments, &mut output); + let recovery_spans = output + .iter() + .filter_map(|item| match item { + ParsedTopItem::Error { span, .. } => Some(*span), + _ => None, + }) + .collect::>(); + tracing::debug!( + target: "parser", + bytes = src.len(), + tokens = token_count, + items = output.len(), + recovered_items = recovery_spans.len(), + parse_errors = parse_errors.len(), + lex_errors = errors.len(), + "parsed top-level items" + ); + + let lex_error_spans = errors.iter().map(|error| error.span).collect::>(); + errors.extend( + parse_errors + .into_iter() + .map(parse_error_from_rich) + .filter(|err| { + !recovery_spans + .iter() + .any(|recovery| span_contains(*recovery, err.span)) + && !lex_error_spans.iter().any(|lex_error| { + lex_error_suppresses_parse_error(src, *lex_error, err.span) + }) + }), + ); + errors.extend( + recovery_spans + .into_iter() + .filter(|span| { + !lex_error_spans + .iter() + .any(|lex_error| lex_error_suppresses_parse_error(src, *lex_error, *span)) + }) + .map(|span| ParsedError::new(span, top_level_recovery_message(src, span))), + ); + + ParseOutput { output, errors } +} + +fn attach_leading_comments<'src>( + source: &'src str, + comments: &[ParsedSourceComment<'src>], + items: &mut [ParsedTopItem<'src>], +) { + for item in items { + let (span, leading_comments) = match item { + ParsedTopItem::Import { + span, + leading_comments, + .. + } + | ParsedTopItem::Export { + span, + leading_comments, + .. + } + | ParsedTopItem::Pragma { + span, + leading_comments, + .. + } + | ParsedTopItem::TypeAlias { + span, + leading_comments, + .. + } + | ParsedTopItem::Adt { + span, + leading_comments, + .. + } + | ParsedTopItem::Class { + span, + leading_comments, + .. + } + | ParsedTopItem::Instance { + span, + leading_comments, + .. + } + | ParsedTopItem::Contract { + span, + leading_comments, + .. + } + | ParsedTopItem::Function { + span, + leading_comments, + .. + } + | ParsedTopItem::Error { + span, + leading_comments, + } => (*span, leading_comments), + }; + *leading_comments = comments_directly_before(source, comments, span.start); + + match item { + ParsedTopItem::Adt { ctors, .. } => { + attach_adt_constructor_comments(source, comments, ctors); + } + ParsedTopItem::Class { methods, .. } => { + for method in methods { + method.leading_comments = + comments_directly_before(source, comments, method.sig.span.start); + } + } + ParsedTopItem::Instance { methods, .. } => { + for method in methods { + method.leading_comments = + comments_directly_before(source, comments, method.span.start); + } + } + ParsedTopItem::Contract { fields, items, .. } => { + for field in fields { + field.leading_comments = + comments_directly_before(source, comments, field.span.start); + } + for item in items { + let (span, leading_comments) = match item { + ParsedContractItem::Function(function) => { + (function.span, &mut function.leading_comments) + } + ParsedContractItem::TypeAlias { + span, + leading_comments, + .. + } + | ParsedContractItem::Adt { + span, + leading_comments, + .. + } + | ParsedContractItem::Error { + span, + leading_comments, + } => (*span, leading_comments), + }; + *leading_comments = comments_directly_before(source, comments, span.start); + if let ParsedContractItem::Adt { ctors, .. } = item { + attach_adt_constructor_comments(source, comments, ctors); + } + } + } + _ => {} + } + } +} + +fn attach_adt_constructor_comments<'src>( + source: &'src str, + comments: &[ParsedSourceComment<'src>], + ctors: &mut [ParsedAdtCtor<'src>], +) { + for ctor in ctors { + let introducer = ctor + .introducer + .expect("ADT parser must retain each constructor introducer"); + if introducer.start >= ctor.span.start { + // Solidity-style enum constructors begin directly at their name + // (and a struct's implicit constructor spans the outer item), so + // there is no separate `=`/`|` introducer before the constructor + // span as there was in the Classic surface. + ctor.leading_comments = comments_directly_before(source, comments, introducer.start); + continue; + } + let trailing_comments = + comments_directly_after_introducer(source, comments, introducer, ctor.span.start); + let next_start = trailing_comments + .first() + .map_or(ctor.span.start, |comment| comment.span.start); + let introducer_gap_is_direct = source + .get(introducer.end..next_start) + .is_some_and(|gap| gap.chars().all(char::is_whitespace) && line_break_count(gap) <= 1); + + let mut leading_comments = if introducer_gap_is_direct { + comments_directly_before(source, comments, introducer.start) + } else { + Vec::new() + }; + leading_comments.extend(trailing_comments); + ctor.leading_comments = leading_comments; + } +} + +fn comments_directly_before<'src>( + source: &'src str, + comments: &[ParsedSourceComment<'src>], + declaration_start: usize, +) -> Vec> { + comments_directly_before_since(source, comments, declaration_start, 0, None) +} + +fn comments_directly_after_introducer<'src>( + source: &'src str, + comments: &[ParsedSourceComment<'src>], + introducer: LexSpan, + declaration_start: usize, +) -> Vec> { + comments_directly_before_since( + source, + comments, + declaration_start, + introducer.end, + Some(introducer.end), + ) +} + +fn comments_directly_before_since<'src>( + source: &'src str, + comments: &[ParsedSourceComment<'src>], + declaration_start: usize, + minimum_start: usize, + allowed_line_prefix_end: Option, +) -> Vec> { + let mut cursor = declaration_start; + let mut attached = Vec::new(); + let first_candidate = comments.partition_point(|comment| comment.span.start < minimum_start); + let past_last_candidate = + comments.partition_point(|comment| comment.span.end <= declaration_start); + + for comment in comments[first_candidate..past_last_candidate].iter().rev() { + debug_assert!(comment.span.end <= cursor); + + let Some(gap) = source.get(comment.span.end..cursor) else { + break; + }; + if !gap.chars().all(char::is_whitespace) + || line_break_count(gap) > 1 + || comment_has_code_before_it_on_line( + source, + comments, + *comment, + allowed_line_prefix_end, + ) + { + break; + } + + attached.push(*comment); + cursor = comment.span.start; + } + + attached.reverse(); + attached +} + +fn comment_has_code_before_it_on_line( + source: &str, + comments: &[ParsedSourceComment<'_>], + comment: ParsedSourceComment<'_>, + allowed_line_prefix_end: Option, +) -> bool { + let line_start = source[..comment.span.start] + .rfind(['\n', '\r']) + .map_or(0, |index| index + 1); + let mut cursor = allowed_line_prefix_end + .filter(|end| line_start <= *end && *end <= comment.span.start) + .unwrap_or(line_start); + let first_candidate = comments.partition_point(|previous| previous.span.end <= cursor); + let past_last_candidate = + comments.partition_point(|previous| previous.span.start < comment.span.start); + + for previous in &comments[first_candidate..past_last_candidate] { + if previous.span.start < line_start || previous.span.end > comment.span.start { + continue; + } + if source[cursor..previous.span.start] + .chars() + .any(|ch| !ch.is_whitespace()) + { + return true; + } + cursor = previous.span.end; + } + + source[cursor..comment.span.start] + .chars() + .any(|ch| !ch.is_whitespace()) +} + +fn line_break_count(text: &str) -> usize { + let bytes = text.as_bytes(); + let mut count = 0; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + count += 1; + index += 1; + } + b'\r' => { + count += 1; + index += usize::from(bytes.get(index + 1) == Some(&b'\n')) + 1; + } + _ => index += 1, + } + } + count +} + +/// Parses statements inside a function or lambda body span. +/// +/// `body_span` is the absolute span of the outer braces in `source`. Returned +/// statement spans remain absolute to the source file; lowering later converts +/// them to offsets relative to the body anchor. +pub(crate) fn parse_body_statements<'src>( + source: &'src str, + body_span: LexSpan, +) -> ParseOutput> { + if body_span.end <= body_span.start + 2 { + tracing::trace!( + target: "parser", + start = body_span.start, + end = body_span.end, + "parsed empty body" + ); + return ParseOutput { + output: Vec::new(), + errors: Vec::new(), + }; + } + + let inner_start = body_span.start + 1; + let inner_end = body_span.end - 1; + let Some(inner_source) = source.get(inner_start..inner_end) else { + trace_recovery("invalid_body_span", body_span); + return ParseOutput { + output: vec![ParsedStmt { + span: body_span, + kind: ParsedStmtKind::Error, + }], + errors: vec![ParsedError::new(body_span, "invalid function body span")], + }; + }; + + // The full-source tokenization owns lexer diagnostics. Body re-tokenization + // still needs their spans to suppress parser cascades, but returning the + // same diagnostics here would duplicate them in `parse_diagnostics`. + let (tokens, lex_errors, mut nesting_errors) = tokenize_with_base(inner_source, inner_start); + let token_snapshot = tokens.clone(); + let token_count = tokens.len(); + let stream = chumsky::input::Stream::from_iter(tokens) + .map((inner_start..inner_end).into(), |(tok, span): (_, _)| { + (tok, span) + }); + let (output, parse_errors) = parsed_stmt_parser() + .repeated() + .collect::>() + .parse(stream) + .into_output_errors(); + tracing::trace!( + target: "parser", + start = body_span.start, + end = body_span.end, + tokens = token_count, + statements = output.as_ref().map_or(0, Vec::len), + parse_errors = parse_errors.len(), + lex_errors = lex_errors.len(), + "parsed body statements" + ); + let lex_error_spans = lex_errors + .iter() + .map(|error| error.span) + .collect::>(); + let parse_errors = parse_errors + .into_iter() + .map(parse_error_from_rich) + .map(|error| refine_body_parse_error(&token_snapshot, error)) + .filter(|error| { + !lex_error_spans + .iter() + .any(|lex_error| lex_error_suppresses_parse_error(source, *lex_error, error.span)) + }) + .collect::>(); + nesting_errors.extend(suppress_body_cascades(parse_errors)); + + ParseOutput { + output: output.unwrap_or_default(), + errors: nesting_errors, + } +} + +#[cfg(test)] +mod tests { + use chumsky::prelude::*; + + use super::{ + MAX_SYNTAX_NESTING, + errors::parse_error_from_rich, + parse_body_statements, parse_supported_items, + recovery::suppress_body_cascades, + tokenize::{tokenize, tokenize_with_base}, + yul::parsed_yul_expr_parser, + }; + use crate::{lexer::Token, types::*}; + + #[test] + fn yul_call_in_assignment_parses() { + let source = "function f() { assembly { res := add(x, y) } }"; + let parsed = parse_supported_items(source); + assert!( + parsed.errors.is_empty(), + "top-level errors: {:?}", + parsed.errors + ); + let body_span = match parsed.output.as_slice() { + [ParsedTopItem::Function { body_span, .. }] => *body_span, + other => panic!("unexpected parse output: {other:?}"), + }; + let body = parse_body_statements(source, body_span); + assert!(body.errors.is_empty(), "body errors: {:?}", body.errors); + } + + #[test] + fn yul_call_expression_parses() { + let source = "add(x, y)"; + let (tokens, errors) = tokenize(source); + assert!(errors.is_empty(), "token errors: {:?}", errors); + assert!( + matches!( + tokens.first().map(|(tok, _)| tok), + Some(Token::Ident(name)) if *name == "add" + ), + "unexpected first token: {:?}", + tokens.first().map(|(tok, _)| tok) + ); + let stream = chumsky::input::Stream::from_iter(tokens) + .map((0..source.len()).into(), |(tok, span): (_, _)| (tok, span)); + let (output, parse_errors) = parsed_yul_expr_parser().parse(stream).into_output_errors(); + assert!( + parse_errors.is_empty(), + "parse errors: {:?}", + parse_errors + .into_iter() + .map(parse_error_from_rich) + .collect::>() + ); + assert!(output.is_some(), "expected parsed output"); + } + + #[test] + fn unicode_identifier_parses() { + let source = "function fλ(x: word) returns (word) { return x; }"; + let parsed = parse_supported_items(source); + assert!( + parsed.errors.is_empty(), + "top-level errors: {:?}", + parsed.errors + ); + assert!(matches!( + parsed.output.as_slice(), + [ParsedTopItem::Function { sig, .. }] if sig.name.0 == "fλ" + )); + } + + #[test] + fn parenthesized_single_pattern_parses_as_grouping() { + let source = "\ +{ + match (p) { + case (y) { return y; } + case ((), (x, z)) { return x; } + } +}"; + let body = parse_body_statements(source, (0..source.len()).into()); + assert!(body.errors.is_empty(), "body errors: {:?}", body.errors); + + let ParsedStmtKind::Match { arms, .. } = &body.output[0].kind else { + panic!("expected match statement"); + }; + + let ParsedPatKind::Var((name, _)) = &arms[0].pats[0].kind else { + panic!("expected grouped pattern to parse as a variable"); + }; + assert_eq!(*name, "y"); + + let ParsedPatKind::Tuple(elems) = &arms[1].pats[0].kind else { + panic!("expected nested tuple pattern to stay a tuple"); + }; + assert_eq!(elems.len(), 2); + } + + #[test] + fn qualified_constructor_patterns_parse() { + let source = "\ +{ + match (mmx) { + case Option.None { return x; } + case Option.Some(Option.None) { return x; } + case y { return y; } + } +}"; + let body = parse_body_statements(source, (0..source.len()).into()); + assert!(body.errors.is_empty(), "body errors: {:?}", body.errors); + + let ParsedStmtKind::Match { arms, .. } = &body.output[0].kind else { + panic!("expected match statement"); + }; + + let ParsedPatKind::Ctor { + qualifiers, + name: (name, _), + args, + .. + } = &arms[0].pats[0].kind + else { + panic!("expected qualified nullary constructor pattern"); + }; + assert_eq!( + qualifiers.iter().map(|(name, _)| *name).collect::>(), + vec!["Option"] + ); + assert_eq!((*name, args.len()), ("None", 0)); + + let ParsedPatKind::Ctor { args, .. } = &arms[1].pats[0].kind else { + panic!("expected qualified constructor pattern with args"); + }; + assert!(matches!( + args[0].kind, + ParsedPatKind::Ctor { + ref qualifiers, + .. + } if !qualifiers.is_empty() + )); + + assert!(matches!( + arms[2].pats[0].kind, + ParsedPatKind::Var((name, _)) if name == "y" + )); + } + + #[test] + fn import_with_alias_parses() { + let parsed = parse_supported_items("import * as Bits from math.bits;"); + assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors); + + match parsed.output.as_slice() { + [ + ParsedTopItem::Import { + external, + path, + alias, + selector, + hiding, + .. + }, + ] => { + assert!(external.is_none(), "expected non-external import"); + assert_eq!( + path.iter().map(|(name, _)| *name).collect::>(), + vec!["math", "bits"] + ); + assert_eq!(alias.as_ref().map(|(name, _)| *name), Some("Bits")); + assert!(selector.is_none(), "expected no selector"); + assert!(hiding.is_empty(), "expected no hidden items"); + } + other => panic!("unexpected parse output: {other:?}"), + } + } + + #[test] + fn solidity_pragma_accepts_tilde_version_constraint() { + let parsed = parse_supported_items("pragma solidity ~0.8.23;"); + assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors); + + assert!(matches!( + parsed.output.as_slice(), + [ParsedTopItem::Pragma { + name: ("solidity", _), + items, + .. + }] if items.is_empty() + )); + } + + #[test] + fn tilde_outside_pragma_remains_a_parse_error() { + let source = "{ let value = ~; return 0; }"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + let tilde = source.find('~').expect("tilde"); + + assert!( + parsed.errors.iter().any(|error| { + error.span.start == tilde && error.message.contains("unexpected `~`") + }), + "missing parser diagnostic for unsupported tilde expression: {:#?}", + parsed.errors + ); + } + + #[test] + fn plain_import_uses_unqualified_public_surface() { + let parsed = parse_supported_items("import math.bits;"); + assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors); + + let [ + ParsedTopItem::Import { + alias, selector, .. + }, + ] = parsed.output.as_slice() + else { + panic!("unexpected parse output: {:?}", parsed.output); + }; + assert!(alias.is_none(), "plain import must not bind a namespace"); + assert!( + matches!(selector, Some(ParsedImportSelector::Wildcard)), + "plain import must import the public surface" + ); + } + + #[test] + fn import_with_selected_items_parses() { + let parsed = parse_supported_items("import {addWord, subWord} from math.words;"); + assert!(parsed.errors.is_empty(), "errors: {:?}", parsed.errors); + + match parsed.output.as_slice() { + [ + ParsedTopItem::Import { + external, + path, + alias, + selector, + hiding, + .. + }, + ] => { + assert!(external.is_none(), "expected non-external import"); + assert_eq!( + path.iter().map(|(name, _)| *name).collect::>(), + vec!["math", "words"] + ); + assert!(alias.is_none(), "expected no alias"); + assert!(hiding.is_empty(), "expected no hidden items"); + let ParsedImportSelector::Names(selected) = + selector.as_ref().expect("expected selector") + else { + panic!("expected selected-name import"); + }; + assert_eq!( + selected + .iter() + .map(|name| name.name.name.as_str()) + .collect::>(), + vec!["addWord", "subWord"] + ); + } + other => panic!("unexpected parse output: {other:?}"), + } + } + + #[test] + fn legacy_wildcard_and_hiding_import_is_rejected() { + let parsed = parse_supported_items( + "// migrate-syntax: keep-legacy-negative\nimport glob.{*} hiding {drop};", + ); + assert!( + !parsed.errors.is_empty(), + "legacy import unexpectedly parsed" + ); + } + + #[test] + fn legacy_import_and_export_operator_names_are_rejected() { + let parsed = parse_supported_items( + "// migrate-syntax: keep-legacy-negative\nimport math.{pow, (^^)};\nexport { f, (^^) };", + ); + assert!( + !parsed.errors.is_empty(), + "legacy syntax unexpectedly parsed" + ); + } + + #[test] + fn legacy_declaration_spellings_are_rejected() { + for source in [ + "// migrate-syntax: keep-legacy-negative\nfunction id(x: T) -> T { return x; }", + "// migrate-syntax: keep-legacy-negative\npublic function f() {}", + "// migrate-syntax: keep-legacy-negative\nforall T. function id(x: T) returns (T) { return x; }", + "// migrate-syntax: keep-legacy-negative\ndata Option(T) = None | Some(T);", + "// migrate-syntax: keep-legacy-negative\nforall T. class T: Eq {}", + "// migrate-syntax: keep-legacy-negative\ninstance word: Eq {}", + ] { + let parsed = parse_supported_items(source); + assert!( + !parsed.errors.is_empty(), + "legacy declaration unexpectedly parsed: {source}" + ); + } + } + + #[test] + fn top_level_function_attributes_are_accepted() { + for attribute in [ + "public", "external", "internal", "private", "pure", "view", "payable", + ] { + let source = format!("function f() {attribute} {{}}"); + let parsed = parse_supported_items(&source); + assert!( + parsed.errors.is_empty(), + "top-level `{attribute}` should use the Solidity-style postfix spelling: {:?}", + parsed.errors + ); + } + } + + #[test] + fn reserved_entrypoint_and_literal_names_are_rejected_for_functions() { + for name in ["fallback", "true", "false"] { + let source = format!("function {name}() {{}}"); + let parsed = parse_supported_items(&source); + assert!( + parsed + .errors + .iter() + .any(|error| error.message.contains("reserved")), + "`{name}` unexpectedly parsed as an ordinary function name: {:?}", + parsed.errors + ); + } + } + + #[test] + fn removed_legacy_keywords_are_available_as_identifiers() { + let parsed = parse_supported_items( + "function class(data: word) returns (word) { let forall = data; let instance = forall; return instance; }", + ); + assert!( + parsed.errors.is_empty(), + "removed syntax words should not stay reserved: {:?}", + parsed.errors + ); + } + + #[test] + fn from_is_contextual_to_imports() { + let parsed = parse_supported_items( + "function from(x: word) returns (word) { return Generic.from(x); }", + ); + assert!( + parsed.errors.is_empty(), + "`from` should remain usable as a function or member name: {:?}", + parsed.errors + ); + } + + #[test] + fn legacy_body_spellings_are_rejected() { + for source in [ + "{ let value = raw : word; }", + "{ match value { | _ => return 0; } }", + "{ if condition { return 0; } }", + "{ let value = .Some(1); }", + ] { + let parsed = parse_body_statements(source, (0..source.len()).into()); + assert!( + !parsed.errors.is_empty(), + "legacy statement unexpectedly parsed: {source}" + ); + } + } + + #[test] + fn import_with_trailing_dot_is_rejected() { + let parsed = parse_supported_items("import foo.;"); + assert!( + !parsed.errors.is_empty(), + "expected parse errors for invalid import" + ); + } + + #[test] + fn lexical_error_does_not_hide_independent_top_level_parse_error() { + let parsed = parse_supported_items("#\nfunction ok() {}\nfunction broken( { }\n"); + + assert!( + parsed + .errors + .iter() + .any(|error| error.message.contains("invalid token `#`")), + "missing lexer diagnostic: {:#?}", + parsed.errors + ); + assert!( + parsed.errors.iter().any(|error| { + error.message.contains("could not parse top-level item") + || error.message.contains("parse error") + }), + "independent declaration error was suppressed: {:#?}", + parsed.errors + ); + } + + #[test] + fn lexical_error_does_not_hide_independent_body_parse_error() { + let source = "{\n#\nlet broken = ;\n}"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + + assert!( + parsed + .errors + .iter() + .any(|error| error.span.start >= source.find("broken").unwrap()), + "independent statement error was suppressed: {:#?}", + parsed.errors + ); + } + + #[test] + fn lexical_error_suppresses_only_its_adjacent_body_cascade() { + let source = "{ let value = #; return 0; }"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + + let semicolon = source.find(';').expect("initializer semicolon"); + assert!( + parsed + .errors + .iter() + .all(|error| error.span.start != semicolon), + "the removed lexer token should not also report its parser cascade: {:#?}", + parsed.errors + ); + } + + #[test] + fn lexical_error_suppresses_a_same_line_cascade_reported_before_it() { + let source = "{ let value = 1 § 2; return value; }"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + + assert!( + parsed.errors.is_empty(), + "unexpected cascade: {:#?}", + parsed.errors + ); + } + + #[test] + fn lexical_error_does_not_hide_a_next_line_top_level_error() { + let source = "#\n;\n"; + let parsed = parse_supported_items(source); + let semicolon = source.find(';').expect("standalone semicolon"); + + assert!( + parsed + .errors + .iter() + .any(|error| error.message.contains("invalid token `#`")), + "missing lexer diagnostic: {:#?}", + parsed.errors + ); + assert!( + parsed + .errors + .iter() + .any(|error| error.span.start == semicolon), + "the independent next-line parse error was suppressed: {:#?}", + parsed.errors + ); + } + + #[test] + fn body_tokenization_enforces_the_delimiter_nesting_limit_directly() { + let mut source = String::new(); + source.push_str(&"(".repeat(MAX_SYNTAX_NESTING + 1)); + source.push('0'); + source.push_str(&")".repeat(MAX_SYNTAX_NESTING + 1)); + + let (_tokens, lexer_errors, errors) = tokenize_with_base(&source, 17); + + assert!(lexer_errors.is_empty()); + assert!( + errors.iter().any(|error| error + .message + .contains("delimiter nesting exceeds the compiler limit")), + "missing body-local nesting diagnostic: {:#?}", + errors + ); + assert!(errors.iter().all(|error| error.span.start >= 17)); + } + + #[test] + fn direct_body_parse_reports_its_own_nesting_guard() { + let mut source = "{".to_owned(); + source.push_str(&"(".repeat(MAX_SYNTAX_NESTING + 1)); + source.push('0'); + source.push_str(&")".repeat(MAX_SYNTAX_NESTING + 1)); + source.push('}'); + + let parsed = parse_body_statements(&source, (0..source.len()).into()); + + assert!( + parsed.errors.iter().any(|error| error + .message + .contains("delimiter nesting exceeds the compiler limit")), + "missing body-local nesting diagnostic: {:#?}", + parsed.errors + ); + } + + #[test] + fn deeply_nested_generic_type_hits_the_nesting_guard() { + let mut source = "alias Deep = ".to_owned(); + source.push_str(&"Box<".repeat(MAX_SYNTAX_NESTING + 1)); + source.push_str("word"); + source.push_str(&">".repeat(MAX_SYNTAX_NESTING + 1)); + source.push(';'); + + let (tokens, errors) = tokenize(&source); + + assert!( + errors.iter().any(|error| error + .message + .contains("generic argument nesting exceeds the compiler limit")), + "missing generic nesting diagnostic: {:#?}", + errors + ); + assert!(tokens.is_empty(), "unsafe token stream was not discarded"); + } + + #[test] + fn comparison_and_shift_operators_are_not_generic_nesting() { + let mut source = "{ sink(".to_owned(); + for index in 0..=MAX_SYNTAX_NESTING { + if index > 0 { + source.push(','); + } + source.push_str("left < right"); + } + source.push_str("); let shifted = left << 1; return shifted > right >> 1; }"); + + let parsed = parse_body_statements(&source, (0..source.len()).into()); + + assert!( + parsed.errors.is_empty(), + "angle-bracket operators were mistaken for generic delimiters: {:#?}", + parsed.errors + ); + } + + #[test] + fn expression_statement_requires_trailing_semicolon() { + let source = "{ f() }"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + + assert!( + !parsed.errors.is_empty(), + "semicolon-less expression statement unexpectedly parsed" + ); + } + + #[test] + fn independent_same_line_body_errors_are_preserved() { + let source = "{ let first = ; let second = ; }"; + let parsed = parse_body_statements(source, (0..source.len()).into()); + let first = source.find(';').expect("first invalid initializer"); + let second = source.rfind(';').expect("second invalid initializer"); + + assert!( + parsed.errors.iter().any(|error| error.span.start == first), + "missing first error: {:#?}", + parsed.errors + ); + assert!( + parsed.errors.iter().any(|error| error.span.start == second), + "same-line second error was suppressed: {:#?}", + parsed.errors + ); + } + + #[test] + fn cascade_filter_preserves_disjoint_same_line_errors() { + let errors = suppress_body_cascades(vec![ + ParsedError::new((10..11).into(), "first independent error"), + ParsedError::new((30..31).into(), "second independent error"), + ]); + + assert_eq!( + errors.len(), + 2, + "disjoint errors were collapsed: {errors:#?}" + ); + } +} diff --git a/crates/parser/src/parse/recovery.rs b/crates/parser/src/parse/recovery.rs new file mode 100644 index 00000000..a80577fb --- /dev/null +++ b/crates/parser/src/parse/recovery.rs @@ -0,0 +1,194 @@ +use super::errors::token_found_description; +use crate::{lexer::Token, types::*}; + +#[inline] +pub(super) fn trace_recovery(kind: &'static str, span: LexSpan) { + tracing::trace!( + target: "parser::recovery", + kind, + start = span.start, + end = span.end, + "parser recovery" + ); +} + +fn preview_span_source(source: &str, span: LexSpan, max_chars: usize) -> Option { + let snippet = source.get(span.start..span.end)?.trim(); + if snippet.is_empty() { + return None; + } + + let single_line = snippet.replace('\n', " "); + let compact = single_line.split_whitespace().collect::>().join(" "); + if compact.is_empty() { + return None; + } + + let mut preview = compact.chars().take(max_chars).collect::(); + if compact.chars().count() > max_chars { + preview.push_str("..."); + } + Some(preview) +} + +pub(super) fn top_level_recovery_message(source: &str, span: LexSpan) -> String { + let expected = "`import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function`"; + match preview_span_source(source, span, 48) { + Some(preview) => format!( + "could not parse top-level item near `{preview}`; expected a declaration starting with {expected}" + ), + None => format!( + "could not parse top-level item; expected a declaration starting with {expected}" + ), + } +} + +pub(super) fn span_contains(outer: LexSpan, inner: LexSpan) -> bool { + outer.start <= inner.start && inner.end <= outer.end +} + +pub(super) fn spans_overlap(lhs: LexSpan, rhs: LexSpan) -> bool { + lhs.start < rhs.end && rhs.start < lhs.end +} + +pub(super) fn lex_error_suppresses_parse_error( + source: &str, + lex_error: LexSpan, + parse_error: LexSpan, +) -> bool { + // Dropping an invalid token can make the statement parser report the + // beginning of that same source line, rather than the missing token's + // position. Treat errors on the affected line as one lexical cascade, + // while preserving structural errors on every other line. + if line_index(source, lex_error.start) == line_index(source, parse_error.start) { + return true; + } + if spans_overlap(lex_error, parse_error) + || (lex_error.start <= parse_error.start && parse_error.start <= lex_error.end) + { + return true; + } + if lex_error.end > parse_error.start { + return false; + } + source + .get(lex_error.end..parse_error.start) + .is_some_and(|gap| { + gap.chars() + .all(|ch| ch.is_whitespace() && ch != '\n' && ch != '\r') + }) +} + +fn line_index(source: &str, offset: usize) -> usize { + source[..offset.min(source.len())] + .bytes() + .filter(|byte| *byte == b'\n') + .count() +} + +fn is_statement_start_token(token: &Token<'_>) -> bool { + matches!( + token, + Token::Let + | Token::Return + | Token::Match + | Token::For + | Token::While + | Token::If + | Token::Unchecked + | Token::Assembly + | Token::LBrace + | Token::Break + | Token::Continue + | Token::Revert + ) +} + +pub(super) fn refine_body_parse_error<'src>( + tokens: &[(Token<'src>, LexSpan)], + error: ParsedError, +) -> ParsedError { + let Some(idx) = tokens.iter().position(|(_, span)| *span == error.span) else { + return error; + }; + + match &tokens[idx].0 { + Token::Let => refine_let_parse_error(tokens, idx).unwrap_or(error), + Token::Match => refine_match_parse_error(tokens, idx).unwrap_or(error), + _ => error, + } +} + +fn refine_let_parse_error<'src>( + tokens: &[(Token<'src>, LexSpan)], + let_idx: usize, +) -> Option { + let assignment_idx = tokens[let_idx + 1..] + .iter() + .position(|(token, _)| matches!(token, Token::Eq | Token::ColonEq)) + .map(|idx| let_idx + 1 + idx)?; + + if let Some((Token::Semi, semi_span)) = tokens.get(assignment_idx + 1) { + return Some( + ParsedError::new(*semi_span, "parse error: unexpected `;`") + .with_label("unexpected token") + .with_note("expecting expression after `=`"), + ); + } + + for (token, span) in &tokens[assignment_idx + 1..] { + if matches!(token, Token::Semi | Token::RBrace) { + return None; + } + if is_statement_start_token(token) { + return Some( + ParsedError::new( + *span, + format!("parse error: unexpected {}", token_found_description(token)), + ) + .with_label("unexpected token") + .with_note("expecting `;` after let statement"), + ); + } + } + + None +} + +fn refine_match_parse_error<'src>( + tokens: &[(Token<'src>, LexSpan)], + match_idx: usize, +) -> Option { + let brace_idx = tokens[match_idx + 1..] + .iter() + .position(|(token, _)| matches!(token, Token::LBrace)) + .map(|idx| match_idx + 1 + idx)?; + let rbrace_span = match tokens.get(brace_idx + 1) { + Some((Token::RBrace, span)) => *span, + _ => return None, + }; + let lbrace_span = tokens[brace_idx].1; + Some( + ParsedError::new( + LexSpan::from(lbrace_span.start..rbrace_span.end), + "match statement requires at least one arm", + ) + .with_label("empty match arm list") + .with_note("add a `case pattern { ... }` or `default { ... }` arm"), + ) +} + +pub(super) fn suppress_body_cascades(mut errors: Vec) -> Vec { + errors.sort_by_key(|error| (error.span.start, error.span.end)); + + let mut filtered: Vec = Vec::with_capacity(errors.len()); + for error in errors { + let should_suppress = filtered.last().is_some_and(|previous| { + span_contains(previous.span, error.span) || spans_overlap(previous.span, error.span) + }); + if !should_suppress { + filtered.push(error); + } + } + filtered +} diff --git a/crates/parser/src/parse/stmt.rs b/crates/parser/src/parse/stmt.rs new file mode 100644 index 00000000..007ebad8 --- /dev/null +++ b/crates/parser/src/parse/stmt.rs @@ -0,0 +1,486 @@ +use chumsky::{input::ValueInput, prelude::*}; +use hir::ast::function; + +use super::{ + common::*, + expr_pat::{parsed_expr_parser, parsed_pat_parser}, + types::type_parser, + yul::parsed_yul_stmt_parser, +}; +use crate::{lexer::Token, types::*}; + +fn assign_op_parser<'src, I>() +-> impl Parser<'src, I, ParsedSpanned<'src, ParsedAssignOp>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Eq) + .to(ParsedAssignOp::Eq) + .or(just(Token::PlusEq).to(ParsedAssignOp::AddEq)) + .or(just(Token::MinusEq).to(ParsedAssignOp::SubEq)) + .or(just(Token::CaretEq).to(ParsedAssignOp::BitXorEq)) + .or(just(Token::AmpEq).to(ParsedAssignOp::BitAndEq)) + .or(just(Token::PipeEq).to(ParsedAssignOp::BitOrEq)) + .or(just(Token::PercentEq).to(ParsedAssignOp::ModEq)) + .map_with(|op, e| ParsedSpanned::new(op, e.span())) +} + +fn assign_stmt_kind<'src>( + lhs: ParsedExpr<'src>, + rhs: Option<(ParsedSpanned<'src, ParsedAssignOp>, ParsedExpr<'src>)>, +) -> ParsedStmtKind<'src> { + match rhs { + Some((op, rhs)) => { + // Match the reference frontend: compound assignment is ordinary + // assignment whose right-hand side is the corresponding binary + // operator expression. This keeps type-class resolution and + // backend specialization identical to `lhs = lhs op rhs`. + let Some(bin_op) = compound_bin_op(op.elem) else { + return ParsedStmtKind::Assign { + op: ParsedAssignOp::Eq, + lhs, + rhs, + }; + }; + let span = LexSpan::from(lhs.span.start..rhs.span.end); + let lhs_read = lhs.clone(); + let rhs = ParsedExpr { + span, + kind: ParsedExprKind::BinOp { + lhs: Box::new(lhs_read), + op: ParsedSpanned::new(bin_op, op.span), + rhs: Box::new(rhs), + }, + }; + ParsedStmtKind::Assign { + op: ParsedAssignOp::Eq, + lhs, + rhs, + } + } + None => ParsedStmtKind::Expr(lhs), + } +} + +fn compound_bin_op(op: ParsedAssignOp) -> Option { + match op { + ParsedAssignOp::Eq => None, + ParsedAssignOp::AddEq => Some(function::BinOp::Add), + ParsedAssignOp::SubEq => Some(function::BinOp::Sub), + ParsedAssignOp::BitXorEq => Some(function::BinOp::BitXor), + ParsedAssignOp::BitAndEq => Some(function::BinOp::BitAnd), + ParsedAssignOp::BitOrEq => Some(function::BinOp::BitOr), + ParsedAssignOp::ModEq => Some(function::BinOp::Mod), + } +} + +fn parsed_binding_pat_parser<'src, I>() -> impl Parser<'src, I, ParsedPat<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + recursive(|pat| { + let var = ident_parser().map(|name| ParsedPat { + span: name.1, + kind: ParsedPatKind::Var(name), + }); + let tuple_or_paren = pat + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|pats, e| match <[_; 1]>::try_from(pats) { + Ok([pat]) => pat, + Err(pats) => ParsedPat { + span: e.span(), + kind: ParsedPatKind::Tuple(pats), + }, + }) + .boxed(); + + tuple_or_paren.or(var).boxed() + }) + .labelled("binding pattern") + .as_context() +} + +fn parsed_let_binding_parser<'src, I>() -> impl Parser<'src, I, ParsedStmt<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Let) + .ignore_then(comptime_kw_parser().or_not()) + .then(parsed_binding_pat_parser()) + .then(just(Token::Colon).ignore_then(type_parser()).or_not()) + .then(just(Token::Eq).ignore_then(parsed_expr_parser()).or_not()) + .validate(|(((comptime, pat), ty), init), e, emitter| { + let span = e.span(); + let kind = match pat.kind { + ParsedPatKind::Var(name) => ParsedStmtKind::Let { + comptime, + name, + ty, + init, + }, + ParsedPatKind::Tuple(elems) => { + if let Some(comptime) = comptime { + emitter.emit(Rich::custom( + comptime, + "`comptime` tuple destructuring is not supported", + )); + ParsedStmtKind::Error + } else if let Some(init) = init { + ParsedStmtKind::LetPattern { + pat: ParsedPat { + span: pat.span, + kind: ParsedPatKind::Tuple(elems), + }, + ty, + init, + } + } else { + emitter.emit(Rich::custom( + pat.span, + "tuple destructuring binding requires an initializer", + )); + ParsedStmtKind::Error + } + } + _ => { + emitter.emit(Rich::custom( + pat.span, + "let binding must use an identifier or tuple pattern", + )); + ParsedStmtKind::Error + } + }; + ParsedStmt { span, kind } + }) +} + +#[derive(Debug, Clone)] +enum ParsedSurfaceMatchArm<'src> { + Case { + span: LexSpan, + pat: ParsedPat<'src>, + body: Vec>, + }, + Default { + span: LexSpan, + kw: LexSpan, + body: Vec>, + }, +} + +fn lower_surface_match_arm<'src>( + scrutinee_count: usize, + arm: ParsedSurfaceMatchArm<'src>, +) -> ParsedMatchArm<'src> { + match arm { + ParsedSurfaceMatchArm::Case { span, pat, body } => { + let pats = if scrutinee_count > 1 { + match pat { + ParsedPat { + kind: ParsedPatKind::Tuple(elems), + .. + } => elems, + pat => vec![pat], + } + } else { + vec![pat] + }; + ParsedMatchArm { span, pats, body } + } + ParsedSurfaceMatchArm::Default { span, kw, body } => ParsedMatchArm { + span, + pats: (0..scrutinee_count) + .map(|_| ParsedPat { + span: kw, + kind: ParsedPatKind::Wildcard, + }) + .collect(), + body, + }, + } +} + +fn parsed_empty_revert<'src>(span: LexSpan) -> ParsedStmt<'src> { + let zero = || ParsedYulExpr { + span, + kind: ParsedYulExprKind::Lit(ParsedYulLitKind::Number("0")), + }; + let call = ParsedYulExpr { + span, + kind: ParsedYulExprKind::Call { + name: ("revert", span), + args: vec![zero(), zero()], + }, + }; + ParsedStmt { + span, + kind: ParsedStmtKind::Assembly { + body: vec![ParsedYulStmt { + span, + kind: ParsedYulStmtKind::Expr(call), + }], + }, + } +} + +fn parsed_for_assign_or_expr_parser<'src, I>() +-> impl Parser<'src, I, ParsedStmt<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + parsed_expr_parser() + .then(assign_op_parser().then(parsed_expr_parser()).or_not()) + .map_with(|(lhs, rhs), e| ParsedStmt { + span: e.span(), + kind: assign_stmt_kind(lhs, rhs), + }) +} + +pub(super) fn parsed_stmt_parser<'src, I>() +-> impl Parser<'src, I, ParsedStmt<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + recursive(|stmt| { + let match_arm_body = stmt + .clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .boxed(); + let case_arm = just(Token::Case) + .ignore_then(parsed_pat_parser()) + .then(match_arm_body.clone()) + .map_with(|(pat, body), e| ParsedSurfaceMatchArm::Case { + span: e.span(), + pat, + body, + }) + .boxed(); + let default_arm = just(Token::Default) + .map_with(|_, e| e.span()) + .then(match_arm_body) + .map_with(|(kw, body), e| ParsedSurfaceMatchArm::Default { + span: e.span(), + kw, + body, + }) + .boxed(); + let match_arm = choice((case_arm, default_arm)).boxed(); + + let let_stmt = parsed_let_binding_parser() + .then_ignore(just(Token::Semi)) + .map_with(|mut stmt, e| { + stmt.span = e.span(); + stmt + }) + .boxed(); + + let return_stmt = just(Token::Return) + .ignore_then(parsed_expr_parser().or_not()) + .then_ignore(just(Token::Semi)) + .map_with(|expr, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Return(expr), + }) + .boxed(); + + let match_stmt = just(Token::Match) + .ignore_then( + parsed_expr_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)), + ) + .then( + match_arm + .repeated() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|(scrutinees, arms), e| { + let scrutinee_count = scrutinees.len(); + let arms = arms + .into_iter() + .map(|arm| lower_surface_match_arm(scrutinee_count, arm)) + .collect(); + ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Match { scrutinees, arms }, + } + }) + .boxed(); + + let for_item = parsed_let_binding_parser() + .or(parsed_for_assign_or_expr_parser()) + .boxed(); + let for_items = for_item + .separated_by(just(Token::Comma)) + .collect::>() + .boxed(); + let for_stmt = just(Token::For) + .ignore_then( + for_items + .clone() + .then_ignore(just(Token::Semi)) + .then(parsed_expr_parser()) + .then_ignore(just(Token::Semi)) + .then(for_items) + .delimited_by(just(Token::LParen), just(Token::RParen)), + ) + .then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|(((init, cond), post), body), e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::For { + init, + cond, + post, + body, + }, + }) + .boxed(); + + let while_stmt = just(Token::While) + .ignore_then( + parsed_expr_parser().delimited_by(just(Token::LParen), just(Token::RParen)), + ) + .then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|(cond, body), e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::For { + init: Vec::new(), + cond, + post: Vec::new(), + body, + }, + }) + .boxed(); + + let if_stmt = just(Token::If) + .ignore_then( + parsed_expr_parser().delimited_by(just(Token::LParen), just(Token::RParen)), + ) + .then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .then( + just(Token::Else) + .ignore_then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .or_not(), + ) + .map_with(|((cond, then_body), else_body), e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::If { + cond, + then_body, + else_body, + }, + }) + .boxed(); + + let unchecked_stmt = just(Token::Unchecked) + .ignore_then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|body, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Block { body }, + }) + .boxed(); + + let assembly_stmt = just(Token::Assembly) + .ignore_then( + parsed_yul_stmt_parser() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|body, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Assembly { body }, + }) + .boxed(); + + let revert_stmt = just(Token::Revert) + .then_ignore(just(Token::Semi)) + .map_with(|_, e| parsed_empty_revert(e.span())) + .boxed(); + + let block_stmt = stmt + .clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map_with(|body, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Block { body }, + }) + .boxed(); + + let break_stmt = just(Token::Break) + .then_ignore(just(Token::Semi)) + .map_with(|_, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Break, + }) + .boxed(); + let continue_stmt = just(Token::Continue) + .then_ignore(just(Token::Semi)) + .map_with(|_, e| ParsedStmt { + span: e.span(), + kind: ParsedStmtKind::Continue, + }) + .boxed(); + let assign_or_expr = parsed_expr_parser() + .then(assign_op_parser().then(parsed_expr_parser()).or_not()) + .then_ignore(just(Token::Semi)) + .map_with(|(lhs, rhs), e| ParsedStmt { + span: e.span(), + kind: assign_stmt_kind(lhs, rhs), + }) + .boxed(); + + choice(( + let_stmt, + return_stmt, + match_stmt, + for_stmt, + while_stmt, + if_stmt, + unchecked_stmt, + assembly_stmt, + revert_stmt, + block_stmt, + break_stmt, + continue_stmt, + assign_or_expr, + )) + }) + .labelled("statement") +} diff --git a/crates/parser/src/parse/tokenize.rs b/crates/parser/src/parse/tokenize.rs new file mode 100644 index 00000000..4dcad2af --- /dev/null +++ b/crates/parser/src/parse/tokenize.rs @@ -0,0 +1,330 @@ +use logos::Logos; + +use super::{ + MAX_EXPRESSION_NESTING, MAX_SYNTAX_NESTING, errors::lex_error, recovery::trace_recovery, +}; +use crate::{ + lexer::{LexedCommentKind, Token}, + types::*, +}; + +#[cfg(test)] +pub(super) fn tokenize<'src>(src: &'src str) -> (Vec<(Token<'src>, LexSpan)>, Vec) { + let (mut tokens, _, mut errors) = tokenize_impl(src, 0); + + truncate_excessive_nesting(&mut tokens, &mut errors); + (tokens, errors) +} + +pub(super) fn tokenize_with_comments<'src>( + src: &'src str, +) -> ( + Vec<(Token<'src>, LexSpan)>, + Vec>, + Vec, +) { + let (mut tokens, comments, mut errors) = tokenize_impl(src, 0); + truncate_excessive_nesting(&mut tokens, &mut errors); + (tokens, comments, errors) +} + +fn truncate_excessive_nesting( + tokens: &mut Vec<(Token<'_>, LexSpan)>, + errors: &mut Vec, +) { + let mut depth = 0usize; + let mut conditional_depth = 0usize; + let mut conditional_bases = Vec::new(); + for (token, span) in tokens.iter() { + match token { + Token::Question => { + conditional_depth += 1; + if conditional_depth > MAX_EXPRESSION_NESTING { + let span = *span; + trace_recovery("nesting_limit", span); + errors.push(ParsedError::new( + span, + format!( + "conditional expression nesting exceeds the compiler limit of {MAX_EXPRESSION_NESTING}" + ), + )); + tokens.clear(); + return; + } + } + Token::LParen | Token::LBracket => { + depth += 1; + if depth > MAX_SYNTAX_NESTING { + let span = *span; + trace_recovery("nesting_limit", span); + errors.push(ParsedError::new( + span, + format!( + "delimiter nesting exceeds the compiler limit of {MAX_SYNTAX_NESTING}" + ), + )); + tokens.clear(); + return; + } + conditional_bases.push(conditional_depth); + } + Token::LBrace => { + depth += 1; + if depth > MAX_SYNTAX_NESTING { + let span = *span; + trace_recovery("nesting_limit", span); + errors.push(ParsedError::new( + span, + format!( + "delimiter nesting exceeds the compiler limit of {MAX_SYNTAX_NESTING}" + ), + )); + tokens.clear(); + return; + } + conditional_bases.push(0); + conditional_depth = 0; + } + Token::RParen | Token::RBrace | Token::RBracket => { + depth = depth.saturating_sub(1); + conditional_depth = conditional_bases.pop().unwrap_or(0); + } + Token::Comma | Token::Semi => { + conditional_depth = conditional_bases.last().copied().unwrap_or(0); + } + _ => {} + } + } + + if let Some(span) = excessive_type_argument_nesting(tokens) { + trace_recovery("nesting_limit", span); + errors.push(ParsedError::new( + span, + format!("generic argument nesting exceeds the compiler limit of {MAX_SYNTAX_NESTING}"), + )); + tokens.clear(); + } +} + +/// Returns the first angle bracket which exceeds the parser's recursion limit. +/// +/// `<` and `>` are also expression operators, so treating every occurrence as +/// a delimiter would make a long (but shallow) sequence of comparisons look +/// recursively nested. Start tracking only where the surrounding tokens +/// establish a type or generic-binder context; once inside such a list, nested +/// lists are unambiguous. Adjacent `<<` tokens are shifts, never generic +/// delimiters. +fn excessive_type_argument_nesting(tokens: &[(Token<'_>, LexSpan)]) -> Option { + let mut angle_depth = 0usize; + + for (index, (token, span)) in tokens.iter().enumerate() { + match token { + Token::Less if !is_left_shift_token(tokens, index) => { + if angle_depth > 0 || starts_type_argument_list(tokens, index) { + angle_depth += 1; + if angle_depth > MAX_SYNTAX_NESTING { + return Some(*span); + } + } + } + Token::Greater if angle_depth > 0 => angle_depth -= 1, + // A generic list cannot cross any of these boundaries. Resetting + // also keeps an incomplete type from making later expressions + // appear nested inside it. + Token::Semi | Token::LBrace | Token::RBrace => angle_depth = 0, + _ => {} + } + } + + None +} + +fn is_left_shift_token(tokens: &[(Token<'_>, LexSpan)], index: usize) -> bool { + matches!( + index.checked_sub(1).and_then(|index| tokens.get(index)), + Some((Token::Less, _)) + ) || matches!(tokens.get(index + 1), Some((Token::Less, _))) +} + +fn starts_type_argument_list(tokens: &[(Token<'_>, LexSpan)], less_index: usize) -> bool { + let Some((Token::Ident(_), _)) = less_index + .checked_sub(1) + .and_then(|index| tokens.get(index)) + else { + return false; + }; + + let mut name_start = less_index - 1; + while name_start >= 2 + && matches!(&tokens[name_start - 1].0, Token::Dot) + && matches!(&tokens[name_start - 2].0, Token::Ident(_)) + { + name_start -= 2; + } + + token_position_starts_type(tokens, name_start) +} + +fn token_position_starts_type(tokens: &[(Token<'_>, LexSpan)], type_start: usize) -> bool { + let Some(previous_index) = type_start.checked_sub(1) else { + return false; + }; + + match &tokens[previous_index].0 { + Token::Colon => !has_unclosed_conditional_before(tokens, previous_index), + Token::Is | Token::As | Token::FatArrow | Token::Where | Token::At | Token::Comptime => { + true + } + Token::Impl => true, + Token::Function + | Token::Enum + | Token::Struct + | Token::Trait + | Token::Contract + | Token::Interface + | Token::Library => true, + Token::Eq => declaration_contains_before(tokens, previous_index, Token::Alias), + Token::Greater => declaration_contains_before(tokens, previous_index, Token::Impl), + Token::LParen | Token::Comma => { + enclosing_parenthesis_starts_type_list(tokens, previous_index) + } + _ => false, + } +} + +fn has_unclosed_conditional_before(tokens: &[(Token<'_>, LexSpan)], boundary_index: usize) -> bool { + tokens[..boundary_index] + .iter() + .rev() + .take_while(|(token, _)| { + !matches!( + token, + Token::Semi | Token::LBrace | Token::RBrace | Token::Comma + ) + }) + .any(|(token, _)| matches!(token, Token::Question)) +} + +fn declaration_contains_before( + tokens: &[(Token<'_>, LexSpan)], + boundary_index: usize, + expected: Token<'_>, +) -> bool { + tokens[..boundary_index] + .iter() + .rev() + .take_while(|(token, _)| !matches!(token, Token::Semi | Token::LBrace | Token::RBrace)) + .any(|(token, _)| token == &expected) +} + +fn enclosing_parenthesis_starts_type_list( + tokens: &[(Token<'_>, LexSpan)], + before_type_index: usize, +) -> bool { + let mut depth = 0usize; + let mut open_index = None; + + for index in (0..=before_type_index).rev() { + match &tokens[index].0 { + Token::RParen => depth += 1, + Token::LParen if depth == 0 => { + open_index = Some(index); + break; + } + Token::LParen => depth -= 1, + _ => {} + } + } + + let Some(open_index) = open_index else { + return false; + }; + let Some((introducer, _)) = open_index + .checked_sub(1) + .and_then(|index| tokens.get(index)) + else { + return false; + }; + + match introducer { + Token::Returns | Token::Function => true, + Token::Ident("mapping") => true, + // A parenthesized tuple can itself occur wherever a type starts. + _ if token_position_starts_type(tokens, open_index) => true, + // Enum constructor payloads are type lists. Restrict this to an enum + // declaration so an ordinary call expression is not misclassified. + Token::Ident(_) => declaration_contains_before(tokens, open_index, Token::Enum), + _ => false, + } +} + +pub(super) fn tokenize_with_base<'src>( + src: &'src str, + base_offset: usize, +) -> ( + Vec<(Token<'src>, LexSpan)>, + Vec, + Vec, +) { + let (mut tokens, _, lexer_errors) = tokenize_impl(src, base_offset); + let mut nesting_errors = Vec::new(); + truncate_excessive_nesting(&mut tokens, &mut nesting_errors); + (tokens, lexer_errors, nesting_errors) +} + +fn tokenize_impl<'src>( + src: &'src str, + base_offset: usize, +) -> ( + Vec<(Token<'src>, LexSpan)>, + Vec>, + Vec, +) { + let mut tokens = Vec::new(); + let mut errors = Vec::new(); + let mut lexer = Token::lexer(src).spanned(); + + for (tok, span) in lexer.by_ref() { + let raw_span = span.clone(); + let span = LexSpan::from((span.start + base_offset)..(span.end + base_offset)); + match tok { + Ok(tok) => tokens.push((tok, span)), + Err(err) => { + trace_recovery("invalid_token", span); + errors.push(lex_error(src, raw_span.start, raw_span.end, span, err)); + } + } + } + + let comments = lexer + .extras + .comments + .iter() + .map(|comment| { + let (kind, text_start, text_end) = match comment.kind { + LexedCommentKind::Line => ( + ParsedSourceCommentKind::Line, + comment.range.start + 2, + comment.range.end, + ), + LexedCommentKind::Block => ( + ParsedSourceCommentKind::Block, + comment.range.start + 2, + comment.range.end - 2, + ), + }; + let text = src + .get(text_start..text_end) + .expect("lexer produced a comment range outside its source"); + ParsedSourceComment { + kind, + text, + span: LexSpan::from( + (comment.range.start + base_offset)..(comment.range.end + base_offset), + ), + } + }) + .collect(); + + (tokens, comments, errors) +} diff --git a/crates/parser/src/parse/types.rs b/crates/parser/src/parse/types.rs new file mode 100644 index 00000000..2542e5da --- /dev/null +++ b/crates/parser/src/parse/types.rs @@ -0,0 +1,381 @@ +use chumsky::{input::ValueInput, prelude::*}; +use hir::ast::{function::FunctionMutability, ty::FunctionTypeVisibility}; + +use super::common::*; +use crate::{lexer::Token, types::*}; + +#[derive(Debug, Clone, Copy)] +enum ParsedArraySuffix { + Dynamic(LexSpan), + Fixed { + length: Option, + brackets_span: LexSpan, + }, +} + +pub(super) fn type_parser<'src, I>() -> impl Parser<'src, I, ParsedTy<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + recursive(|ty| { + let angle_args = ty + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::Less), just(Token::Greater)) + .map_with(|args, e| (args, e.span())) + .or_not() + .boxed(); + + let named_type = qualified_ident_parser() + .then(angle_args) + .map_with(|(mut path, args), e| { + let name = path.pop().expect("qualified path has at least one segment"); + let (args, args_span) = args + .map(|(args, span)| (args, Some(span))) + .unwrap_or_else(|| (Vec::new(), None)); + ParsedTy { + span: e.span(), + kind: ParsedTyKind::Named { + qualifiers: path, + name, + args, + args_span, + }, + } + }) + .boxed(); + + let grouped_types = ty + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|elems, e| (elems, e.span())) + .boxed(); + + let tuple_type = grouped_types + .clone() + .map(|(elems, paren_span)| ParsedTy { + span: paren_span, + kind: ParsedTyKind::Tuple { elems }, + }) + .boxed(); + + // `mapping` remains an ordinary type constructor in HIR. The surface + // syntax merely changes its two arguments from `mapping(K, V)` to + // Solidity's `mapping(K => V)`. + let mapping_kw = select! { + Token::Ident(name) if name == "mapping" => name, + } + .map_with(|name, e| (name, e.span())) + .boxed(); + let mapping_type = mapping_kw + .clone() + .then_ignore(just(Token::LParen)) + .rewind() + .ignore_then(mapping_kw) + .then( + ty.clone() + .then_ignore(just(Token::FatArrow)) + .then(ty.clone()) + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map_with(|types, e| (types, e.span())), + ) + .map_with( + |((name, name_span), ((key, value), args_span)), e| ParsedTy { + span: e.span(), + kind: ParsedTyKind::Named { + qualifiers: Vec::new(), + name: (name, name_span), + args: vec![key, value], + args_span: Some(args_span), + }, + }, + ) + .boxed(); + + let function_visibility = choice(( + just(Token::Internal).map_with(|_, e| (FunctionTypeVisibility::Internal, e.span())), + just(Token::External).map_with(|_, e| (FunctionTypeVisibility::External, e.span())), + )) + .or_not() + .boxed(); + let function_mutability = choice(( + just(Token::Pure).map_with(|_, e| (FunctionMutability::Pure, e.span())), + just(Token::View).map_with(|_, e| (FunctionMutability::View, e.span())), + just(Token::Payable).map_with(|_, e| (FunctionMutability::Payable, e.span())), + )) + .or_not() + .boxed(); + let function_returns = just(Token::Returns) + .ignore_then(grouped_types.clone()) + .or_not() + .boxed(); + let function_type = just(Token::Function) + .ignore_then(grouped_types.clone()) + .then(function_visibility) + .then(function_mutability) + .then(function_returns) + .map_with( + |((((params, params_span), visibility), mutability), returns), e| { + let function_span: LexSpan = e.span(); + let ret = match returns { + Some((elems, span)) => match <[_; 1]>::try_from(elems) { + Ok([ret]) => ret, + Err(elems) => ParsedTy { + span, + kind: ParsedTyKind::Tuple { elems }, + }, + }, + None => { + let end = function_span.end; + ParsedTy { + span: LexSpan::from(end..end), + kind: ParsedTyKind::Tuple { elems: Vec::new() }, + } + } + }; + ParsedTy { + span: function_span, + kind: ParsedTyKind::Fn { + params, + params_span, + visibility, + mutability, + ret: Box::new(ret), + }, + } + }, + ) + .boxed(); + + // `comptime T` and proxy types are retained as noncanonical Solcore + // extensions because they carry semantics that the new surface-syntax + // proposal does not replace. + let comptime_type = comptime_kw_parser() + .then(ty.clone()) + .map_with(|(kw, inner), e| ParsedTy { + span: e.span(), + kind: ParsedTyKind::Comptime { + kw, + inner: Box::new(inner), + }, + }) + .boxed(); + + let atom_type = recursive(|atom| { + let proxy_type = just(Token::At) + .map_with(|_, e| e.span()) + .then(atom) + .map_with(|(at, inner), e| ParsedTy { + span: e.span(), + kind: ParsedTyKind::Proxy { + at, + inner: Box::new(inner), + }, + }) + .boxed(); + + proxy_type + .or(function_type) + .or(mapping_type) + .or(tuple_type) + .or(named_type) + }) + .boxed(); + + let atom_type = comptime_type.or(atom_type).boxed(); + + let dynamic_array_suffix = just(Token::LBracket) + .then_ignore(just(Token::RBracket)) + .map_with(|_, e| ParsedArraySuffix::Dynamic(e.span())); + let fixed_array_suffix = just(Token::LBracket) + .ignore_then(select! { + Token::Number(length) => length, + Token::HexLit(length) => length, + }) + .then_ignore(just(Token::RBracket)) + .validate(|literal, e, emitter| { + let brackets_span = e.span(); + let parsed = literal.strip_prefix("0x").map_or_else( + || literal.parse::(), + |hex| u64::from_str_radix(hex, 16), + ); + let length = match parsed { + Ok(0) => { + emitter.emit(Rich::custom( + brackets_span, + "fixed array length must be greater than zero", + )); + None + } + Ok(length) => Some(length), + Err(_) => { + emitter.emit(Rich::custom( + brackets_span, + format!( + "fixed array length `{literal}` exceeds the supported u64 range" + ), + )); + None + } + }; + ParsedArraySuffix::Fixed { + length, + brackets_span, + } + }); + let array_suffix = choice((dynamic_array_suffix, fixed_array_suffix)); + let array_type = atom_type + .foldl_with(array_suffix.repeated(), |inner, suffix, e| { + let span = e.span(); + match suffix { + ParsedArraySuffix::Dynamic(brackets_span) => ParsedTy { + span, + kind: ParsedTyKind::Named { + qualifiers: Vec::new(), + // Dynamic Solidity arrays keep the standard + // library's nominal `DynArray` representation. + name: ("DynArray", brackets_span), + args_span: Some(inner.span), + args: vec![inner], + }, + }, + ParsedArraySuffix::Fixed { + length: Some(length), + brackets_span, + } => ParsedTy { + span, + kind: ParsedTyKind::FixedArray { + element: Box::new(inner), + length, + brackets_span, + }, + }, + ParsedArraySuffix::Fixed { length: None, .. } => ParsedTy { + span, + kind: ParsedTyKind::Error, + }, + } + }) + .boxed(); + + let location = select! { + Token::Ident(name) if matches!(name, "memory" | "storage" | "calldata") => name, + } + .map_with(|name, e| (name, e.span())) + .or_not(); + + array_type + .then(location) + .map_with(|(inner, location), e| match location { + Some((name, name_span)) => { + let args_span = inner.span; + ParsedTy { + span: e.span(), + kind: ParsedTyKind::Named { + qualifiers: Vec::new(), + name: (name, name_span), + args: vec![inner], + args_span: Some(args_span), + }, + } + } + None => inner, + }) + }) + .labelled("type") + .as_context() +} + +pub(super) fn pred_parser<'src, I>() -> impl Parser<'src, I, ParsedPred<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + type_parser() + .then_ignore(just(Token::Colon)) + .then(trait_ref_parser()) + .map(|(ty, (class, args, args_span))| ParsedPred { + ty, + class, + args, + args_span, + }) + .labelled("trait constraint") + .as_context() + .boxed() +} + +/// Parses an optional generic binder list such as ``. +/// +/// The empty vector represents an absent list. An explicitly empty `<>` list +/// is rejected so callers do not need to distinguish two spellings. +pub(super) fn type_param_list_parser<'src, I>() +-> impl Parser<'src, I, Vec>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + ident_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::Less), just(Token::Greater)) + .or_not() + .map(Option::unwrap_or_default) + .labelled("type parameter list") + .as_context() +} + +/// Parses a trait reference such as `Eq` or `Convert`. +/// +/// The returned tuple contains the trait name, its type arguments, and the +/// span of the optional angle-bracketed argument list. +pub(super) fn trait_ref_parser<'src, I>() +-> impl Parser<'src, I, (SpannedStr<'src>, Vec>, Option), ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + let args = type_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::Less), just(Token::Greater)) + .map_with(|args, e| (args, e.span())) + .or_not(); + + ident_parser() + .then(args) + .map(|(name, args)| { + let (args, args_span) = args + .map(|(args, span)| (args, Some(span))) + .unwrap_or_else(|| (Vec::new(), None)); + (name, args, args_span) + }) + .labelled("trait reference") + .as_context() +} + +/// Parses an optional `where` clause. +pub(super) fn where_clause_parser<'src, I>() +-> impl Parser<'src, I, Vec>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + just(Token::Where) + .ignore_then( + pred_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .allow_trailing() + .collect::>(), + ) + .or_not() + .map(Option::unwrap_or_default) + .labelled("where clause") + .as_context() +} diff --git a/crates/parser/src/parse/yul.rs b/crates/parser/src/parse/yul.rs new file mode 100644 index 00000000..70bdf8bb --- /dev/null +++ b/crates/parser/src/parse/yul.rs @@ -0,0 +1,290 @@ +use chumsky::{input::ValueInput, prelude::*}; + +use super::{common::*, recovery::trace_recovery}; +use crate::{lexer::Token, types::*}; + +fn yul_expr_ident_parser<'src, I>() -> impl Parser<'src, I, SpannedStr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + ident_parser().or( + // `revert` is a Solidity statement keyword in Core source but remains + // an ordinary Yul builtin function name inside `assembly`. + just(Token::Revert).map_with(|_, e| ("revert", e.span())), + ) +} + +fn parsed_yul_lit_parser<'src, I>() -> impl Parser<'src, I, ParsedYulLitKind<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + select! { + Token::Number(n) => ParsedYulLitKind::Number(n), + Token::HexLit(h) => ParsedYulLitKind::Hex(h), + Token::String(s) => ParsedYulLitKind::String(s), + Token::True => ParsedYulLitKind::Bool(true), + Token::False => ParsedYulLitKind::Bool(false), + } + .boxed() +} + +pub(super) fn parsed_yul_expr_parser<'src, I>() +-> impl Parser<'src, I, ParsedYulExpr<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + recursive(|expr| { + let lit = parsed_yul_lit_parser() + .map_with(|lit, e| ParsedYulExpr { + span: e.span(), + kind: ParsedYulExprKind::Lit(lit), + }) + .boxed(); + + let ident_or_call = yul_expr_ident_parser() + .then( + expr.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not(), + ) + .map_with(|(name, args), e| ParsedYulExpr { + span: e.span(), + kind: match args { + Some(args) => ParsedYulExprKind::Call { name, args }, + None => ParsedYulExprKind::Ident(name), + }, + }) + .boxed(); + + let recovery = any() + .and_is( + just(Token::Comma) + .or(just(Token::RParen)) + .or(just(Token::RBrace)) + .not(), + ) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("assembly_expr", span); + ParsedYulExpr { + span, + kind: ParsedYulExprKind::Error, + } + }); + + choice((lit, ident_or_call)).recover_with(via_parser(recovery)) + }) + .labelled("assembly expression") +} + +pub(super) fn parsed_yul_stmt_parser<'src, I>() +-> impl Parser<'src, I, ParsedYulStmt<'src>, ParserErr<'src>> +where + I: ValueInput<'src, Token = Token<'src>, Span = LexSpan>, +{ + recursive(|stmt| { + let block = stmt + .clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map_with(|body, e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Block(body), + }) + .boxed(); + + let let_stmt = just(Token::Let) + .ignore_then( + ident_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>(), + ) + .then( + just(Token::ColonEq) + .ignore_then(parsed_yul_expr_parser()) + .or_not(), + ) + .map_with(|(names, init), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Let { names, init }, + }) + .boxed(); + + let assign = ident_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>() + .then_ignore(just(Token::ColonEq)) + .then(parsed_yul_expr_parser()) + .map_with(|(names, value), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Assign { names, value }, + }) + .boxed(); + + let expr_stmt = parsed_yul_expr_parser() + .map_with(|expr, e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Expr(expr), + }) + .boxed(); + + let return_builtin = just(Token::Return) + .map_with(|_, e| ("return", e.span())) + .then( + parsed_yul_expr_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)), + ) + .map_with(|(name, args), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Expr(ParsedYulExpr { + span: e.span(), + kind: ParsedYulExprKind::Call { name, args }, + }), + }) + .boxed(); + + let if_stmt = just(Token::If) + .ignore_then(parsed_yul_expr_parser()) + .then( + stmt.clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)), + ) + .map_with(|(cond, body), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::If { cond, body }, + }) + .boxed(); + + let stmt_block = stmt + .clone() + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)); + + let for_stmt = just(Token::For) + .ignore_then(stmt_block.clone()) + .then(parsed_yul_expr_parser()) + .then(stmt_block.clone()) + .then(stmt_block.clone()) + .map_with(|(((init, cond), post), body), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::For { + init, + cond, + post, + body, + }, + }) + .boxed(); + + let case = just(Token::Case) + .ignore_then(parsed_yul_lit_parser()) + .then(stmt_block.clone()) + .map_with(|(lit, body), e| ParsedYulCase { + span: e.span(), + lit, + body, + }); + let default = just(Token::Default).ignore_then(stmt_block.clone()); + let switch_stmt = just(Token::Switch) + .ignore_then(parsed_yul_expr_parser()) + .then(case.repeated().collect::>()) + .then(default.or_not()) + .map_with(|((expr, cases), default), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Switch { + expr, + cases, + default, + }, + }) + .boxed(); + + let ident_list = ident_parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)); + let rets = just(Token::Arrow) + .ignore_then( + ident_parser() + .separated_by(just(Token::Comma)) + .at_least(1) + .collect::>(), + ) + .or_not() + .map(|r| r.unwrap_or_default()); + let function_def = just(Token::Function) + .ignore_then(ident_parser()) + .then(ident_list) + .then(rets) + .then(stmt_block) + .map_with(|(((name, params), rets), body), e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::FunctionDef { + name, + params, + rets, + body, + }, + }) + .boxed(); + + let leave = just(Token::Leave).map_with(|_, e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Leave, + }); + let break_ = just(Token::Break).map_with(|_, e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Break, + }); + let continue_ = just(Token::Continue).map_with(|_, e| ParsedYulStmt { + span: e.span(), + kind: ParsedYulStmtKind::Continue, + }); + + let recovery = any() + .and_is(just(Token::RBrace).not()) + .repeated() + .at_least(1) + .map_with(|_, e| { + let span = e.span(); + trace_recovery("assembly_stmt", span); + ParsedYulStmt { + span, + kind: ParsedYulStmtKind::Error, + } + }); + + choice(( + block, + let_stmt, + if_stmt, + for_stmt, + switch_stmt, + function_def, + assign, + return_builtin, + leave, + break_, + continue_, + expr_stmt, + )) + .then_ignore(just(Token::Semi).or_not()) + .recover_with(via_parser(recovery)) + }) + .labelled("assembly statement") +} diff --git a/crates/parser/src/types.rs b/crates/parser/src/types.rs index 8d17998a..722b1511 100644 --- a/crates/parser/src/types.rs +++ b/crates/parser/src/types.rs @@ -1,381 +1,968 @@ +//! Lightweight parsed syntax shared by the grammar and HIR lowerer. +//! +//! These types borrow text from the source string and use absolute lexical +//! spans. They deliberately avoid HIR concepts such as `DefId` and +//! anchor-relative spans; lowering is the boundary that allocates identities, +//! anchors, arenas, and diagnostics. + use chumsky::{extra, prelude::Rich}; -use hir::ast::function; +use hir::ast::{ + function, + item::{ContractKind, FuncKind}, + ty::FunctionTypeVisibility, +}; use crate::lexer::Token; +/// Absolute byte span produced by Chumsky. pub(crate) type LexSpan = chumsky::span::SimpleSpan; +/// Borrowed source string paired with its absolute span. pub(crate) type SpannedStr<'src> = (&'src str, LexSpan); +/// Parser error type used by Chumsky combinators. pub(crate) type ParserErr<'src> = extra::Err>>; +/// Lexical form of a parsed source comment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ParsedSourceCommentKind { + /// A `// ...` comment. + Line, + /// A `/* ... */` comment. + Block, +} + +/// Comment trivia retained while keeping it out of the parser grammar. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ParsedSourceComment<'src> { + /// Lexical comment form. + pub(crate) kind: ParsedSourceCommentKind, + /// Exact text between the outer comment delimiters. + pub(crate) text: &'src str, + /// Absolute span including the comment delimiters. + pub(crate) span: LexSpan, +} + +/// Source metadata shared by item lowerers. +/// +/// Parsed item variants keep these fields directly so the grammar remains +/// convenient to destructure. Lowering groups them when an item's payload +/// would otherwise exceed a readable argument count. +#[derive(Debug, Clone)] +pub(crate) struct ParsedItemMeta<'src> { + /// Span covering the declaration. + pub(crate) span: LexSpan, + /// Consecutive comments directly preceding the declaration. + pub(crate) leading_comments: Vec>, +} + +/// Semantic family selected by a source type declaration keyword. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ParsedTypeAliasKind { + /// `alias Name = Type` is a transparent synonym. + Transparent, + /// `type Name is Type` introduces a nominal user-defined value type. + ValueType, +} + +/// User-facing parse error before conversion to HIR diagnostics. #[derive(Debug, Clone)] pub(crate) struct ParsedError { + /// Absolute source span of the error. pub(crate) span: LexSpan, + /// Human-readable message. pub(crate) message: String, + /// Optional primary label message. + pub(crate) label: Option, + /// Additional explanatory notes. + pub(crate) notes: Vec, } +impl ParsedError { + pub(crate) fn new(span: LexSpan, message: impl Into) -> Self { + Self { + span, + message: message.into(), + label: None, + notes: Vec::new(), + } + } + + pub(crate) fn with_label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub(crate) fn with_note(mut self, note: impl Into) -> Self { + self.notes.push(note.into()); + self + } +} + +/// Parsed output plus recoverable parse errors. #[derive(Debug, Clone)] pub(crate) struct ParseOutput { + /// Successfully parsed nodes, including recovery sentinel nodes. pub(crate) output: Vec, + /// Errors emitted while producing the output. pub(crate) errors: Vec, } +/// Parsed top-level item before HIR lowering. #[derive(Debug, Clone)] pub(crate) enum ParsedTopItem<'src> { + /// Import declaration. Import { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Span of an external-library marker. + external: Option, + /// Imported module path. path: Vec>, + /// Optional module alias. + alias: Option>, + /// Optional selected import list. + selector: Option>, + /// Hidden names. + hiding: Vec, + }, + /// Export declaration. + Export { + /// Span covering the declaration. + span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Export payload. + kind: ParsedExportKind<'src>, }, + /// Pragma declaration. Pragma { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Pragma name. name: SpannedStr<'src>, + /// Pragma items. items: Vec>, }, + /// Type alias declaration. TypeAlias { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Whether this is a transparent alias or a nominal value type. + kind: ParsedTypeAliasKind, + /// Alias name. name: SpannedStr<'src>, + /// Type parameters. ty_params: Vec>, + /// Aliased type. ty: ParsedTy<'src>, }, + /// Algebraic data type declaration. Adt { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Type name. name: SpannedStr<'src>, + /// Type parameters. ty_params: Vec>, + /// Constructors. ctors: Vec>, }, + /// Trait declaration (stored in the existing class representation). Class { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Declared type parameters. type_vars: Vec>, + /// Superclass predicates. super_preds: Vec>, + /// Class head predicate. head: ParsedPred<'src>, - methods: Vec>, + /// Method signature declarations. + methods: Vec>, }, + /// Impl declaration (stored in the existing instance representation). Instance { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Declared type parameters. type_vars: Vec>, + /// Context predicates. preds: Vec>, + /// Span of optional `default`. default_kw: Option, + /// Instance head predicate. head: ParsedPred<'src>, + /// Method implementations. methods: Vec>, }, + /// Contract declaration. Contract { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Solidity-style declaration shell. + kind: ContractKind, + /// Contract name. name: SpannedStr<'src>, + /// Contract type parameters. ty_params: Vec>, + /// Field declarations. fields: Vec>, + /// Contract-local items. items: Vec>, }, + /// Top-level function declaration. Function { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the function. + leading_comments: Vec>, + /// Function signature. sig: ParsedFuncSig<'src>, + /// Absolute span of the body braces. body_span: LexSpan, }, + /// Parser recovery placeholder. Error { + /// Span covering the recovered invalid item. span: LexSpan, + /// Consecutive comments directly preceding the recovered item. + leading_comments: Vec>, }, } +/// Parsed import/export name. +#[derive(Debug, Clone)] +pub(crate) struct ParsedImportName { + /// Textual name, with operators stored without surrounding parentheses. + pub(crate) name: String, + /// Absolute span of the name syntax. + pub(crate) span: LexSpan, + /// Whether the name came from an operator selector. + pub(crate) is_operator: bool, +} + +/// One selected import name. +#[derive(Debug, Clone)] +pub(crate) struct ParsedSelectedName<'src> { + /// Imported name. + pub(crate) name: ParsedImportName, + /// Optional alias. + pub(crate) alias: Option>, + /// Optional constructor selector. + pub(crate) constructors: Option>, +} + +/// Import selector payload. +#[derive(Debug, Clone)] +pub(crate) enum ParsedImportSelector<'src> { + /// Import every public name through the canonical plain-import spelling. + Wildcard, + /// Explicit selected names. + Names(Vec>), +} + +/// Constructor selector payload. +#[derive(Debug, Clone)] +pub(crate) enum ParsedConstructorSelector<'src> { + /// All constructors. + All, + /// Named constructors. + Named(Vec>), +} + +/// One exported item name. +#[derive(Debug, Clone)] +pub(crate) struct ParsedExportName<'src> { + /// Exported name. + pub(crate) name: ParsedImportName, + /// Optional constructor selector. + pub(crate) constructors: Option>, +} + +/// Export declaration payload. +#[derive(Debug, Clone)] +pub(crate) enum ParsedExportKind<'src> { + /// Explicit current-module export list. + List(Vec>), + /// Re-export a whole module. + Module(Vec>), + /// Re-export a module under an alias. + ModuleAs(Vec>, SpannedStr<'src>), + /// Re-export selected items from a module. + ItemsFrom(Vec>, Vec>), +} + +/// Parsed type reference. #[derive(Debug, Clone)] pub(crate) struct ParsedTy<'src> { + /// Absolute span of the type syntax. pub(crate) span: LexSpan, + /// Type payload. pub(crate) kind: ParsedTyKind<'src>, } +/// Parsed type reference payload. #[derive(Debug, Clone)] pub(crate) enum ParsedTyKind<'src> { + /// Named type constructor with optional qualifier path and arguments. Named { + /// Qualifier path before the final name. + qualifiers: Vec>, + /// Final type name. name: SpannedStr<'src>, + /// Type arguments. args: Vec>, - }, + /// Span of the source type-argument syntax, if present. + args_span: Option, + }, + /// Solidity fixed-length array suffix: `Element[N]`. + FixedArray { + /// Array element type. + element: Box>, + /// Strictly positive array length. + length: u64, + /// Span of the complete `[N]` suffix. + brackets_span: LexSpan, + }, + /// Proxy type sugar introduced by `@`. + Proxy { + /// Span of the `@`. + at: LexSpan, + /// Proxied type. + inner: Box>, + }, + /// Function type. Fn { + /// Parameter types. params: Vec>, + /// Span of the source domain type or parameter group. + params_span: LexSpan, + /// Explicit visibility qualifier and its keyword span. + visibility: Option<(FunctionTypeVisibility, LexSpan)>, + /// Explicit state-mutability qualifier and its keyword span. + mutability: Option<(function::FunctionMutability, LexSpan)>, + /// Return type. ret: Box>, }, + /// `comptime` type wrapper. + Comptime { + /// Span of the keyword. + kw: LexSpan, + /// Wrapped type. + inner: Box>, + }, + /// Tuple type syntax. Tuple { + /// Tuple elements. elems: Vec>, }, + /// Parser recovery placeholder. Error, } +/// Parsed trait constraint (stored in the existing class-predicate shape). #[derive(Debug, Clone)] pub(crate) struct ParsedPred<'src> { + /// Main constrained type. pub(crate) ty: ParsedTy<'src>, + /// Trait name. pub(crate) class: SpannedStr<'src>, + /// Additional trait arguments. pub(crate) args: Vec>, + /// Span of the angle-bracketed trait-argument list, if present. + pub(crate) args_span: Option, } +/// Parsed ADT constructor. #[derive(Debug, Clone)] pub(crate) struct ParsedAdtCtor<'src> { + /// Span covering the constructor. pub(crate) span: LexSpan, + /// `=` or `|` token introducing this constructor, filled by the ADT parser. + pub(crate) introducer: Option, + /// Consecutive comments directly preceding the constructor. + pub(crate) leading_comments: Vec>, + /// Constructor name. pub(crate) name: SpannedStr<'src>, + /// Field types. pub(crate) fields: Vec>, + /// Struct field names in source order. + /// + /// Enum constructor payloads are positional and therefore use `None`. + pub(crate) field_names: Option>>, } +/// Parsed method signature declared by a trait. +#[derive(Debug, Clone)] +pub(crate) struct ParsedClassMethod<'src> { + /// Consecutive comments directly preceding the method signature. + pub(crate) leading_comments: Vec>, + /// Method signature. + pub(crate) sig: ParsedFuncSig<'src>, +} + +/// Parsed function parameter. #[derive(Debug, Clone)] pub(crate) enum ParsedFuncParam<'src> { + /// Parameter with a type annotation. Typed { + /// Optional `comptime` keyword span. + comptime: Option, + /// Parameter name. name: SpannedStr<'src>, + /// Parameter type. ty: ParsedTy<'src>, }, + /// Parameter without a type annotation. Untyped { + /// Optional `comptime` keyword span. + comptime: Option, + /// Parameter name. name: SpannedStr<'src>, }, - Error, + /// Parser recovery placeholder. + Error { + /// Span covering the malformed parameter. + span: LexSpan, + }, } +/// Parsed function signature. #[derive(Debug, Clone)] pub(crate) struct ParsedFuncSig<'src> { + /// Span covering the signature. pub(crate) span: LexSpan, + /// Declared type parameters. pub(crate) type_vars: Vec>, + /// Qualifying predicates. pub(crate) preds: Vec>, + /// Optional visibility keyword and span. + pub(crate) visibility: Option<(function::FunctionVisibility, LexSpan)>, + /// Optional state-mutability keyword and span. + pub(crate) mutability: Option<(function::FunctionMutability, LexSpan)>, + /// Function name. pub(crate) name: SpannedStr<'src>, + /// Parameters. pub(crate) params: Vec>, + /// Span of the parameter list. pub(crate) params_span: LexSpan, + /// Optional return type. pub(crate) ret: Option>, + /// Optional names for each top-level return value. + /// + /// The vector is parallel to the source-level `returns (...)` entries. + /// Unnamed entries are represented by `None`; an omitted `returns` clause + /// and an explicit empty result list both have no entries. + pub(crate) ret_names: Vec>>, } +/// Parsed function declaration with an optional unparsed body span. #[derive(Debug, Clone)] pub(crate) struct ParsedFunctionDef<'src> { + /// Span covering the definition. pub(crate) span: LexSpan, + /// Function kind. + pub(crate) kind: FuncKind, + /// Consecutive comments directly preceding the function. + pub(crate) leading_comments: Vec>, + /// Function signature. pub(crate) sig: ParsedFuncSig<'src>, - pub(crate) body_span: LexSpan, + /// Absolute span of the body braces, or `None` for a prototype. + pub(crate) body_span: Option, } +/// Parsed contract field. #[derive(Debug, Clone)] pub(crate) struct ParsedFieldDef<'src> { + /// Span covering the field declaration. pub(crate) span: LexSpan, + /// Consecutive comments directly preceding the field. + pub(crate) leading_comments: Vec>, + /// Field name. pub(crate) name: SpannedStr<'src>, + /// Field type. pub(crate) ty: ParsedTy<'src>, + /// Optional field initializer expression. + pub(crate) init: Option>, } +/// Parsed item inside a contract body. #[derive(Debug, Clone)] pub(crate) enum ParsedContractItem<'src> { + /// Function-like contract member. Function(ParsedFunctionDef<'src>), + /// Contract-local type alias. TypeAlias { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// Whether this is a transparent alias or a nominal value type. + kind: ParsedTypeAliasKind, + /// Alias name. name: SpannedStr<'src>, + /// Type parameters. ty_params: Vec>, + /// Aliased type. ty: ParsedTy<'src>, }, + /// Contract-local ADT. Adt { + /// Span covering the declaration. span: LexSpan, + /// Consecutive comments directly preceding the declaration. + leading_comments: Vec>, + /// ADT name. name: SpannedStr<'src>, + /// Type parameters. ty_params: Vec>, + /// Constructors. ctors: Vec>, }, + /// Parser recovery placeholder. Error { + /// Span covering the malformed contract item. span: LexSpan, + /// Consecutive comments directly preceding the recovered item. + leading_comments: Vec>, }, } +/// Parsed source literal. #[derive(Debug, Clone)] pub(crate) enum ParsedLitKind<'src> { + /// Decimal number literal text. Number(&'src str), + /// Hexadecimal literal text. Hex(&'src str), + /// Quoted string literal text. String(&'src str), } +/// Parsed expression. #[derive(Debug, Clone)] pub(crate) struct ParsedExpr<'src> { + /// Absolute span of the expression. pub(crate) span: LexSpan, + /// Expression payload. pub(crate) kind: ParsedExprKind<'src>, } +/// Parsed expression payload. #[derive(Debug, Clone)] pub(crate) enum ParsedExprKind<'src> { + /// Literal expression. Lit(ParsedLitKind<'src>), + /// Identifier expression. Ident(SpannedStr<'src>), + /// Type proxy expression. + Proxy { + /// Span of the `@`. + at: LexSpan, + /// Proxied type. + ty: ParsedTy<'src>, + }, + /// Lambda expression with an unparsed body span. Lambda { + /// Parameters. params: Vec>, + /// Span of the parameter list. params_span: LexSpan, + /// Optional return type. ret: Option>, + /// Absolute span of the body braces. body_span: LexSpan, }, + /// Binary operator expression. BinOp { + /// Left operand. lhs: Box>, + /// Operator and span. op: ParsedSpanned<'src, function::BinOp>, + /// Right operand. rhs: Box>, }, + /// Indexing expression. Index { + /// Base expression. base: Box>, + /// Index expression. index: Box>, }, + /// Call expression. Call { + /// Callee expression. callee: Box>, + /// Arguments. args: Vec>, }, + /// Field/path selection expression. Field { + /// Base expression. base: Box>, + /// Field name. field: SpannedStr<'src>, }, - TypeAnnot { + /// Explicit type conversion written with `as`. + Conversion { + /// Converted expression. + expr: Box>, + /// Conversion target type. + ty: ParsedTy<'src>, + }, + /// Internal type ascription used by lowering-generated expressions. + /// + /// The surface grammar never constructs this variant. Unlike `Conversion`, + /// it is allowed to guide inference from its target type. + TypeAscription { + /// Ascribed expression. expr: Box>, + /// Expected type for the expression. ty: ParsedTy<'src>, }, + /// Unary operator expression. UnaryOp { + /// Operator and span. op: ParsedSpanned<'src, function::UnOp>, + /// Operand. expr: Box>, }, + /// Conditional expression. If { + /// Condition expression. cond: Box>, + /// Then expression. then_expr: Box>, + /// Else expression. else_expr: Box>, }, + /// Tuple expression. + Tuple(Vec>), + /// Parser recovery placeholder. Error, } +/// Parsed pattern. #[derive(Debug, Clone)] pub(crate) struct ParsedPat<'src> { + /// Absolute span of the pattern. pub(crate) span: LexSpan, + /// Pattern payload. pub(crate) kind: ParsedPatKind<'src>, } +/// Parsed pattern payload. #[derive(Debug, Clone)] pub(crate) enum ParsedPatKind<'src> { + /// `_` wildcard. Wildcard, + /// Variable binder. Var(SpannedStr<'src>), + /// Literal pattern. Lit(ParsedLitKind<'src>), + /// Constructor pattern. Ctor { + /// Leading-dot span for deferred constructor lookup. + leading_dot: Option, + /// Qualifier path before the constructor name. + qualifiers: Vec>, + /// Constructor or variable name. name: SpannedStr<'src>, + /// Constructor argument patterns. args: Vec>, }, + /// `comptime` label pattern. + ComptimeLabel { + /// Span of the `comptime` keyword. + kw: LexSpan, + /// Attached expression. + expr: ParsedExpr<'src>, + }, + /// Tuple pattern. Tuple(Vec>), + /// Parser recovery placeholder. Error, } +/// Parsed match arm. #[derive(Debug, Clone)] pub(crate) struct ParsedMatchArm<'src> { + /// Span covering the arm. pub(crate) span: LexSpan, + /// Patterns matched by the arm. pub(crate) pats: Vec>, + /// Body statements. pub(crate) body: Vec>, } +/// Parsed statement. #[derive(Debug, Clone)] pub(crate) struct ParsedStmt<'src> { + /// Absolute span of the statement. pub(crate) span: LexSpan, + /// Statement payload. pub(crate) kind: ParsedStmtKind<'src>, } +/// Parsed assignment operator. +#[derive(Debug, Clone, Copy)] +pub(crate) enum ParsedAssignOp { + /// `=` assignment. + Eq, + /// `+=` assignment. + AddEq, + /// `-=` assignment. + SubEq, + /// `^=` assignment. + BitXorEq, + /// `&=` assignment. + BitAndEq, + /// `|=` assignment. + BitOrEq, + /// `%=` assignment. + ModEq, +} + +/// Parsed statement payload. #[derive(Debug, Clone)] pub(crate) enum ParsedStmtKind<'src> { + /// Local binding statement. Let { + /// Optional `comptime` keyword span. + comptime: Option, + /// Binder name. name: SpannedStr<'src>, + /// Optional type annotation. ty: Option>, + /// Optional initializer expression. init: Option>, }, + /// Tuple destructuring binding. + /// + /// Lowering rewrites this and the remainder of its lexical block into a + /// single-arm tuple match so the existing pattern binder and scope + /// machinery can represent the declaration without losing scope. + LetPattern { + /// Tuple binding pattern. + pat: ParsedPat<'src>, + /// Optional type annotation for the complete pattern. + ty: Option>, + /// Required initializer expression. + init: ParsedExpr<'src>, + }, + /// Return statement. Return(Option>), + /// Expression statement. Expr(ParsedExpr<'src>), + /// Assignment. Assign { + /// Assignment operator. + op: ParsedAssignOp, + /// Assignment target. lhs: ParsedExpr<'src>, + /// Assigned value. rhs: ParsedExpr<'src>, }, - AddAssign { - lhs: ParsedExpr<'src>, - rhs: ParsedExpr<'src>, - }, - SubAssign { - lhs: ParsedExpr<'src>, - rhs: ParsedExpr<'src>, - }, + /// Match statement. Match { + /// Scrutinee expressions. scrutinees: Vec>, + /// Match arms. arms: Vec>, }, + /// C-style for loop. + For { + /// Initializer statements. + init: Vec>, + /// Condition expression. + cond: ParsedExpr<'src>, + /// Post-iteration statements. + post: Vec>, + /// Body statements. + body: Vec>, + }, + /// Conditional statement. If { + /// Condition expression. cond: ParsedExpr<'src>, + /// Then-body statements. then_body: Vec>, + /// Optional else-body statements. else_body: Option>>, }, + /// Lexical block statement. + Block { + /// Statements inside the block. + body: Vec>, + }, + /// Inline Yul assembly block. Assembly { + /// Parsed Yul statements. body: Vec>, }, + /// Break statement. + Break, + /// Continue statement. + Continue, + /// Parser recovery placeholder. Error, } +/// Parsed Yul literal. #[derive(Debug, Clone)] pub(crate) enum ParsedYulLitKind<'src> { + /// Decimal number literal text. Number(&'src str), + /// Hexadecimal literal text. Hex(&'src str), + /// Quoted string literal text. String(&'src str), + /// Boolean literal. Bool(bool), } +/// Parsed Yul expression. #[derive(Debug, Clone)] pub(crate) struct ParsedYulExpr<'src> { + /// Absolute span of the expression. pub(crate) span: LexSpan, + /// Expression payload. pub(crate) kind: ParsedYulExprKind<'src>, } +/// Parsed Yul expression payload. #[derive(Debug, Clone)] pub(crate) enum ParsedYulExprKind<'src> { + /// Literal expression. Lit(ParsedYulLitKind<'src>), + /// Identifier expression. Ident(SpannedStr<'src>), + /// Function call expression. Call { + /// Callee name. name: SpannedStr<'src>, + /// Arguments. args: Vec>, }, + /// Parser recovery placeholder. Error, } +/// Parsed Yul switch case. #[derive(Debug, Clone)] pub(crate) struct ParsedYulCase<'src> { + /// Span covering the case. pub(crate) span: LexSpan, + /// Matched literal. pub(crate) lit: ParsedYulLitKind<'src>, + /// Case body statements. pub(crate) body: Vec>, } +/// Parsed Yul statement. #[derive(Debug, Clone)] pub(crate) struct ParsedYulStmt<'src> { + /// Absolute span of the statement. pub(crate) span: LexSpan, + /// Statement payload. pub(crate) kind: ParsedYulStmtKind<'src>, } +/// Parsed Yul statement payload. #[derive(Debug, Clone)] pub(crate) enum ParsedYulStmtKind<'src> { + /// Block statement. Block(Vec>), + /// Let statement. Let { + /// Bound names. names: Vec>, + /// Optional initializer. init: Option>, }, + /// Assignment statement. Assign { + /// Assigned names. names: Vec>, + /// Assigned value. value: ParsedYulExpr<'src>, }, + /// Expression statement. Expr(ParsedYulExpr<'src>), + /// Conditional statement. If { + /// Condition expression. cond: ParsedYulExpr<'src>, + /// Body statements. body: Vec>, }, + /// For loop. For { + /// Initializer statements. init: Vec>, + /// Condition expression. cond: ParsedYulExpr<'src>, + /// Post-iteration statements. post: Vec>, + /// Body statements. body: Vec>, }, + /// Switch statement. Switch { + /// Scrutinee expression. expr: ParsedYulExpr<'src>, + /// Explicit cases. cases: Vec>, + /// Optional default body. default: Option>>, }, + /// Function definition. FunctionDef { + /// Function name. name: SpannedStr<'src>, + /// Parameter names. params: Vec>, + /// Return names. rets: Vec>, + /// Function body. body: Vec>, }, + /// Leave statement. Leave, + /// Break statement. Break, + /// Continue statement. Continue, + /// Parser recovery placeholder. Error, } +/// Generic parsed value paired with an absolute span. #[derive(Debug, Clone, Copy)] pub(crate) struct ParsedSpanned<'src, T> { + /// Parsed value. pub(crate) elem: T, + /// Absolute span of the value. pub(crate) span: LexSpan, + /// Marker retaining the source lifetime for borrowed parsed trees. pub(crate) _marker: std::marker::PhantomData<&'src ()>, } impl<'src, T> ParsedSpanned<'src, T> { + /// Creates a spanned parsed value. pub(crate) fn new(elem: T, span: LexSpan) -> Self { Self { elem, diff --git a/crates/parser/tests/def_identity.rs b/crates/parser/tests/def_identity.rs new file mode 100644 index 00000000..9edfb9f1 --- /dev/null +++ b/crates/parser/tests/def_identity.rs @@ -0,0 +1,380 @@ +use hir::{ + anchor::{DefId, DefKind}, + input::SourceFile, +}; +use salsa::Setter; +use solcore_parser::parse_file_to_hir; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl solcore_parser::Db for TestDb {} + +#[derive(Debug, PartialEq, Eq)] +struct DefIdentity { + owner: Option>, + kind: DefKind, + name: Option, + fingerprint: Option, + disambiguator: u32, +} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn def_identity<'db>(db: &'db TestDb, def: DefId<'db>) -> DefIdentity { + DefIdentity { + owner: def.owner(db).map(|owner| Box::new(def_identity(db, owner))), + kind: def.kind(db), + name: def.name(db), + fingerprint: def.fingerprint(db), + disambiguator: def.disambiguator(db).as_u32(), + } +} + +fn all_defs<'db>(db: &'db TestDb, file: SourceFile) -> Vec> { + parse_file_to_hir(db, file) + .def_locations(db) + .entries + .iter() + .map(|entry| entry.def_id) + .collect() +} + +fn defs_by_name<'db>( + db: &'db TestDb, + file: SourceFile, + kind: DefKind, + name: &str, +) -> Vec> { + all_defs(db, file) + .into_iter() + .filter(|def| def.kind(db) == kind && def.name(db).as_deref() == Some(name)) + .collect() +} + +fn defs_by_fingerprint<'db>( + db: &'db TestDb, + file: SourceFile, + kind: DefKind, + fingerprint: &str, +) -> Vec> { + all_defs(db, file) + .into_iter() + .filter(|def| def.kind(db) == kind && def.fingerprint(db).as_deref() == Some(fingerprint)) + .collect() +} + +fn lambda_body_identities(db: &TestDb, file: SourceFile) -> Vec<(String, DefIdentity)> { + let mut bodies = all_defs(db, file) + .into_iter() + .filter(|def| { + def.kind(db) == DefKind::FuncBody && def.name(db).as_deref() == Some("lambda") + }) + .map(|def| { + ( + def.fingerprint(db).expect("lambda body fingerprint"), + def_identity(db, def), + ) + }) + .collect::>(); + bodies.sort_by(|a, b| a.0.cmp(&b.0)); + bodies +} + +#[test] +fn same_named_contract_methods_have_container_relative_def_ids() { + let db = TestDb::default(); + let file = source_file( + &db, + "contract-methods", + "contract A {\n function f() {}\n}\n\ncontract B {\n function f() {}\n}\n", + ); + + let methods = defs_by_name(&db, file, DefKind::Function, "f"); + assert_eq!(methods.len(), 2); + assert_ne!(methods[0], methods[1]); + assert_ne!(methods[0].owner(&db), methods[1].owner(&db)); +} + +#[test] +fn instances_of_same_class_on_different_heads_have_distinct_def_ids() { + let db = TestDb::default(); + let file = source_file( + &db, + "instance-heads", + "trait StorageType {}\n\n\ + impl StorageType {\n function rep(x: word) returns (word) { return x; }\n}\n\n\ + impl StorageType {\n function rep(x: uint) returns (uint) { return x; }\n}\n", + ); + + let instances = defs_by_name(&db, file, DefKind::Instance, "StorageType"); + assert_eq!(instances.len(), 2); + assert_ne!(instances[0], instances[1]); + + let fingerprints = instances + .iter() + .map(|def| def.fingerprint(&db)) + .collect::>(); + assert!(fingerprints.contains(&Some("pred[1]|4:word".to_owned()))); + assert!(fingerprints.contains(&Some("pred[1]|4:uint".to_owned()))); +} + +#[test] +fn instances_with_same_subject_and_different_class_args_have_distinct_def_ids() { + let db = TestDb::default(); + let file = source_file( + &db, + "instance-class-args", + "trait Carrier {}\n\n\ + impl Carrier {}\n\n\ + impl Carrier {}\n", + ); + + let instances = defs_by_name(&db, file, DefKind::Instance, "Carrier"); + assert_eq!(instances.len(), 2); + assert_ne!(instances[0], instances[1]); + + let fingerprints = instances + .iter() + .map(|def| def.fingerprint(&db)) + .collect::>(); + assert!(fingerprints.contains(&Some("pred[2]|4:word|4:uint".to_owned()))); + assert!(fingerprints.contains(&Some("pred[2]|4:word|4:bool".to_owned()))); +} + +#[test] +fn imports_have_structural_def_ids() { + let db = TestDb::default(); + let file = source_file( + &db, + "imports-distinct", + "import * as A from A;\nimport * as B from B;\n", + ); + + let import_a = defs_by_fingerprint(&db, file, DefKind::Import, "A as A"); + let import_b = defs_by_fingerprint(&db, file, DefKind::Import, "B as B"); + assert_eq!(import_a.len(), 1); + assert_eq!(import_b.len(), 1); + assert_ne!(import_a[0], import_b[0]); +} + +#[test] +fn inserting_import_above_keeps_existing_import_identities_stable() { + let mut db = TestDb::default(); + let file = source_file( + &db, + "imports-stable", + "import * as A from A;\nimport * as B from B;\n", + ); + + let before_a = { + let imports = defs_by_fingerprint(&db, file, DefKind::Import, "A as A"); + assert_eq!(imports.len(), 1); + def_identity(&db, imports[0]) + }; + let before_b = { + let imports = defs_by_fingerprint(&db, file, DefKind::Import, "B as B"); + assert_eq!(imports.len(), 1); + def_identity(&db, imports[0]) + }; + + file.set_content(&mut db).to(Some( + "import * as C from C;\nimport * as A from A;\nimport * as B from B;\n".to_owned(), + )); + + let after_a = { + let imports = defs_by_fingerprint(&db, file, DefKind::Import, "A as A"); + assert_eq!(imports.len(), 1); + def_identity(&db, imports[0]) + }; + let after_b = { + let imports = defs_by_fingerprint(&db, file, DefKind::Import, "B as B"); + assert_eq!(imports.len(), 1); + def_identity(&db, imports[0]) + }; + + assert_eq!(after_a, before_a); + assert_eq!(after_b, before_b); +} + +#[test] +fn import_selector_fingerprints_are_structural_and_order_independent() { + let db = TestDb::default(); + let file = source_file( + &db, + "imports-selector-fingerprints", + "import {x as y, op} from A;\n\ + import {op, x as y} from A;\n\ + import {x} from A;\n\ + import {x as y} from A;\n\ + import * as A from A;\n", + ); + + let mut fingerprints = all_defs(&db, file) + .into_iter() + .filter(|def| def.kind(&db) == DefKind::Import) + .map(|def| def.fingerprint(&db).expect("import fingerprint")) + .collect::>(); + + assert_eq!(fingerprints.len(), 5); + fingerprints.sort(); + assert_eq!( + fingerprints + .windows(2) + .filter(|pair| pair[0] == pair[1]) + .count(), + 1 + ); + fingerprints.dedup(); + assert_eq!(fingerprints.len(), 4); +} + +#[test] +fn inserting_preceding_lambda_keeps_existing_lambda_body_identities_stable() { + let mut db = TestDb::default(); + let before_src = "function f(z: word) returns (word) { + let n = lam (x: word) { return x; }; + let m = lam (y: word) { return y; }; + return m(n(z)); + }"; + let file = source_file(&db, "lambda-bodies-stable", before_src); + + let before = lambda_body_identities(&db, file); + assert_eq!(before.len(), 2); + + file.set_content(&mut db).to(Some( + "function f(z: word) returns (word) { + let ignored = lam (q: word) { return q + 1; }; + let n = lam (x: word) { return x; }; + let m = lam (y: word) { return y; }; + return m(n(z)); + }" + .to_owned(), + )); + + let after = lambda_body_identities(&db, file); + assert_eq!(after.len(), 3); + + for (fingerprint, identity) in before { + let after_identity = after + .iter() + .find_map(|(after_fingerprint, after_identity)| { + (after_fingerprint == &fingerprint).then_some(after_identity) + }) + .expect("original lambda fingerprint after insertion"); + assert_eq!(after_identity, &identity); + } +} + +#[test] +fn lambda_body_edit_keeps_lambda_body_identity_stable() { + let mut db = TestDb::default(); + let before_src = "function f(z: word) returns (word) { + let n = lam (x: word) { return x + 1; }; + return n(z); + }"; + let file = source_file(&db, "lambda-body-edit-stable", before_src); + + let before = lambda_body_identities(&db, file); + assert_eq!(before.len(), 1); + + file.set_content(&mut db).to(Some( + "function f(z: word) returns (word) { + let n = lam (x: word) { return x + 2; }; + return n(z); + }" + .to_owned(), + )); + + let after = lambda_body_identities(&db, file); + assert_eq!(after.len(), 1); + assert_eq!(after[0].1, before[0].1); +} + +#[test] +fn inserting_unrelated_item_above_def_keeps_identity_stable() { + let mut db = TestDb::default(); + let file = source_file(&db, "stable-def", "\nfunction target() {}\n"); + + let before = { + let targets = defs_by_name(&db, file, DefKind::Function, "target"); + assert_eq!(targets.len(), 1); + def_identity(&db, targets[0]) + }; + + file.set_content(&mut db).to(Some( + "\nfunction helper() {}\n\nfunction target() {}\n".to_owned(), + )); + + let after = { + let targets = defs_by_name(&db, file, DefKind::Function, "target"); + assert_eq!(targets.len(), 1); + def_identity(&db, targets[0]) + }; + + assert_eq!(after, before); +} + +#[test] +fn leading_whitespace_does_not_change_def_identity() { + let mut db = TestDb::default(); + let file = source_file(&db, "leading-whitespace", "\nfunction target() {}\n"); + + let before = { + let targets = defs_by_name(&db, file, DefKind::Function, "target"); + assert_eq!(targets.len(), 1); + def_identity(&db, targets[0]) + }; + + file.set_content(&mut db) + .to(Some("\n\n\nfunction target() {}\n".to_owned())); + + let after = { + let targets = defs_by_name(&db, file, DefKind::Function, "target"); + assert_eq!(targets.len(), 1); + def_identity(&db, targets[0]) + }; + + assert_eq!(after, before); +} + +#[test] +fn well_formed_program_defs_have_zero_disambiguators() { + let db = TestDb::default(); + let file = source_file( + &db, + "zero-disambiguators", + "trait StorageType {}\n\n\ + impl StorageType {\n function rep(x: word) returns (word) { return x; }\n}\n\n\ + contract Counter {\n function main() returns (word) { return 0; }\n}\n\n\ + function top() {}\n", + ); + + let non_zero = all_defs(&db, file) + .into_iter() + .map(|def| def_identity(&db, def)) + .filter(|identity| identity.disambiguator != 0) + .collect::>(); + + assert_eq!(non_zero, Vec::::new()); +} diff --git a/crates/parser/tests/diagnostics.rs b/crates/parser/tests/diagnostics.rs index 127dc0e0..f07c7f6c 100644 --- a/crates/parser/tests/diagnostics.rs +++ b/crates/parser/tests/diagnostics.rs @@ -1,9 +1,13 @@ -use std::path::Path; +use std::{panic, path::Path, thread}; use annotate_snippets::Renderer; use dir_test::{Fixture, dir_test}; -use hir::{diag::Diagnostic, input::SourceFile}; -use solcore_parser::parse_file_to_hir; +use hir::{ + diag::{AnyDiagnostic, Diagnostic}, + input::SourceFile, + visit::ErrorNode, +}; +use solcore_parser::{parse_diagnostics, parse_file_to_hir}; #[salsa::db] #[derive(Default, Clone)] @@ -15,35 +19,52 @@ struct TestDb { impl salsa::Database for TestDb {} #[salsa::db] -impl hir::Db for TestDb {} +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} #[salsa::db] impl solcore_parser::Db for TestDb {} #[dir_test( - dir: "$CARGO_MANIFEST_DIR/tests/fixtures/fail", - glob: "*.solc" + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/corpus/fail", + glob: "**/*.solc" )] -fn parser_fail_diagnostics(fixture: Fixture<&str>) { +fn parser_corpus_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_assertion(fixture, assert_fail_fixture); +} + +fn assert_fail_fixture(path: &str, content: &str) { let db = TestDb::default(); - let file = fixture_source_file(&db, &fixture); - let _ = parse_file_to_hir(&db, file); - let diagnostics = parse_file_to_hir::accumulated::(&db, file); - assert!( - !diagnostics.is_empty(), - "expected diagnostics for fail fixture `{}`", - fixture.path() - ); - if fixture.path().ends_with("multiple_emitted_errors.solc") { + let file = fixture_source_file(&db, path, content); + let module = parse_file_to_hir(&db, file).module(&db); + let diagnostics = lower_diagnostics(&db, parse_diagnostics(&db, file)); + if diagnostics.is_empty() { + let error_nodes = hir::visit::collect_error_nodes(&db, module); + assert!( + error_nodes.is_empty(), + "expected no HIR Error nodes for semantic fail fixture `{}`\n{}", + path, + render_error_nodes(&db, &error_nodes) + ); + return; + } + + if path.ends_with("multiple_emitted_errors.solc") { assert!( diagnostics.len() > 1, "expected more than one diagnostic for `{}`", - fixture.path() + path ); } let rendered = render_diagnostics(&db, &diagnostics); - assert_snapshot_for_fixture(fixture.path(), &rendered); + assert_snapshot_for_fixture(path, &rendered); } #[dir_test( @@ -51,21 +72,53 @@ fn parser_fail_diagnostics(fixture: Fixture<&str>) { glob: "**/*.solc" )] fn parser_ok_no_diagnostics(fixture: Fixture<&str>) { + run_fixture_assertion(fixture, assert_ok_fixture); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/corpus/ok", + glob: "**/*.solc" +)] +fn parser_corpus_ok_no_diagnostics(fixture: Fixture<&str>) { + run_fixture_assertion(fixture, assert_ok_fixture); +} + +fn assert_ok_fixture(path: &str, content: &str) { let db = TestDb::default(); - let file = fixture_source_file(&db, &fixture); + let file = fixture_source_file(&db, path, content); - let _ = parse_file_to_hir(&db, file).module(&db); - let diagnostics = parse_file_to_hir::accumulated::(&db, file); + let module = parse_file_to_hir(&db, file).module(&db); + let diagnostics = lower_diagnostics(&db, parse_diagnostics(&db, file)); assert!( diagnostics.is_empty(), "expected no diagnostics for ok fixture `{}`\n{}", - fixture.path(), + path, render_diagnostics(&db, &diagnostics) ); + let error_nodes = hir::visit::collect_error_nodes(&db, module); + assert!( + error_nodes.is_empty(), + "expected no HIR Error nodes for ok fixture `{}`\n{}", + path, + render_error_nodes(&db, &error_nodes) + ); } -fn fixture_source_file(db: &TestDb, fixture: &Fixture<&str>) -> SourceFile { - let fixture_path = Path::new(fixture.path()); +fn run_fixture_assertion(fixture: Fixture<&str>, assertion: fn(&str, &str)) { + let path = fixture.path().to_owned(); + let content = fixture.content().to_string(); + let result = thread::Builder::new() + .stack_size(64 * 1024 * 1024) + .spawn(move || assertion(&path, &content)) + .expect("spawn fixture assertion") + .join(); + if let Err(payload) = result { + panic::resume_unwind(payload); + } +} + +fn fixture_source_file(db: &TestDb, path: &str, content: &str) -> SourceFile { + let fixture_path = Path::new(path); let file_name = fixture_path .file_name() .and_then(|name| name.to_str()) @@ -73,10 +126,17 @@ fn fixture_source_file(db: &TestDb, fixture: &Fixture<&str>) -> SourceFile { let url = format!("memory:///{file_name}") .parse() .expect("valid fixture URL"); - SourceFile::new(db, url, Some(fixture.content().to_string())) + SourceFile::new(db, url, Some(content.to_string())) +} + +fn lower_diagnostics(db: &dyn hir::Db, diagnostics: &[AnyDiagnostic]) -> Vec { + diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect() } -fn render_diagnostics(db: &dyn hir::Db, diagnostics: &[&Diagnostic]) -> String { +fn render_diagnostics(db: &dyn hir::Db, diagnostics: &[Diagnostic]) -> String { if diagnostics.is_empty() { return "no diagnostics\n".to_owned(); } @@ -92,6 +152,24 @@ fn render_diagnostics(db: &dyn hir::Db, diagnostics: &[&Diagnostic]) -> String { output } +fn render_error_nodes(db: &dyn hir::Db, errors: &[ErrorNode<'_>]) -> String { + if errors.is_empty() { + return "no HIR Error nodes\n".to_owned(); + } + + let mut output = String::new(); + for error in errors { + let span = error.span.resolve_to_absolute(db); + output.push_str(&format!( + "{} @ {}..{}\n", + error.kind, + span.start().as_u32(), + span.end().as_u32() + )); + } + output +} + fn assert_snapshot_for_fixture(fixture_path: &str, value: &str) { let fixture_path = Path::new(fixture_path); let fixture_dir = fixture_path.parent().expect("fixture parent"); diff --git a/crates/parser/tests/fixtures/corpus/README.md b/crates/parser/tests/fixtures/corpus/README.md new file mode 100644 index 00000000..473a9fa1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/README.md @@ -0,0 +1,7 @@ +This corpus vendors Solcore reference fixtures from Y-Nak/solcore at ac6f8957. + +Sources: +- `ok/std`: copied from `/Users/y_nak/github.com/Y-Nak/solcore/std` +- `ok/test/examples` and `fail/test/examples`: copied from `/Users/y_nak/github.com/Y-Nak/solcore/test/examples` + +The example split is derived by running the ac6f8957 reference binary in frontend mode (`-n -g`) with the vendored reference std. Files in `ok/test/examples` pass that reference frontend run. Files in `fail/test/examples` are rejected by the reference frontend or hit the recorded 60-second timeout. The full per-file verdict is recorded in `reference-frontend.tsv`. Parser snapshots are kept only for fail fixtures that also produce Rust parser diagnostics. diff --git a/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.snap b/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.snap new file mode 100644 index 00000000..5b5f7f47 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.snap @@ -0,0 +1,14 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.solc +--- +error[SC0001]: parse error: unexpected `returns` + --> /parse-error.solc:1:16 + | +1 | function main( returns (word) { return 0; } + | ^^^^^^^ unexpected token + | + = note: expecting `comptime` + = note: keywords cannot be used as identifiers; choose a different name + = note: while parsing function parameter diff --git a/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.solc b/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.solc new file mode 100644 index 00000000..eccfe371 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/diagnostics/parse-error.solc @@ -0,0 +1 @@ +function main( returns (word) { return 0; } diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/Convertible.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/Convertible.solc new file mode 100644 index 00000000..ce7d26d9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/Convertible.solc @@ -0,0 +1,105 @@ +enum Pair { Pair(a, b) } +enum Proxy { Proxy } +enum Unit { Unit } + +trait Typedef { + function abs(x: r) returns (a) ; + function rep(x: a) returns (r) ; +} + + +enum uint16 { uint16(word) } + +impl Typedef { + function abs(r: word) returns (uint16) { return uint16.uint16(r);} + function rep(x: uint16) returns (word) { + match (x) { case uint16.uint16(val) { return val; } } + } +} + +enum uint8 { uint8(word) } + +impl Typedef { + function abs(r: word) returns (uint8) { return uint8.uint8(r);} + function rep(x: uint8) returns (word) { + match (x) { case uint8.uint8(val) { return val; } } + } +} + +enum uint256 { uint256(word) } + +impl Typedef { + function abs(r: word) returns (uint256) { return uint256.uint256(r);} + function rep(x: uint256) returns (word) { + match (x) { case uint256.uint256(val) { return val; } } + } +} + + +function foo(x: word) returns (uint16) { + let result: uint16 = Typedef.abs(x); + return result; +} + + +trait Convertible { + function convert(x: self) returns (r) ; +} + +impl Convertible>, uint16> { + function convert(p: Pair>) returns (uint16) { + match (p) { case Pair.Pair(x, _) { return Typedef.abs(Typedef.rep(x)); } } + } +} + + + +function uint8to16(x: uint8) returns (uint16) { + let proxy: Proxy = Proxy.Proxy; + let result: uint16 = Convertible.convert(Pair.Pair(x,proxy)); + return result; +} + +/* +forall Pair(a,Proxy(b)):Convertible(b). function convert(x:a) -> b { + let proxy : Proxy(b) = Proxy; + let result : b = Convertible.convert(Pair(x,proxy)); + return result; +} +*/ + +function convert(x: a) returns (b) { + let proxy: Proxy = Proxy.Proxy; + let result: b = Convertible.convert(Pair.Pair(x,proxy)); + return result; +} + +function bar(x: Unit) returns (word) { + let result: word = convert(x); + return result; +} + + +impl Convertible>, uint256> { + function convert(p: Pair>) returns (uint256) { + match (p) { case Pair.Pair(x, _) { return Typedef.abs(Typedef.rep(x)); } } + } +} + +impl Convertible>, uint256> { + function convert(p: Pair>) returns (uint256) { + match (p) { case Pair.Pair(x, _) { return Typedef.abs(Typedef.rep(x)); } } + } +} + + + +contract Bar { + +function main() public returns (word) { + let x = Unit.Unit; + let y: word = convert(x); + return y; +} + +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/BadInstance.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/BadInstance.solc new file mode 100644 index 00000000..070beac8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/BadInstance.solc @@ -0,0 +1,15 @@ +trait Enum { + function fromEnum(x: a) returns (word) ; + } + +enum Color { R, G, B } + +enum Bool { False, True } + +impl Enum { + function fromEnum(b: Bool) returns (word) { + match (b) { case Color.R { return 0; } case Color.G { return 1; } } + } +} + + diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/DupFun.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/DupFun.solc new file mode 100644 index 00000000..1159c19c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/DupFun.solc @@ -0,0 +1,11 @@ +function f(x: word) returns (word) { + return x; +} + +function f(x: word) returns (word) { + return 10; +} + +function g(x: word) returns (word) { + return f(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Enum.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Enum.solc new file mode 100644 index 00000000..6ba5dcbc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Enum.solc @@ -0,0 +1,18 @@ +trait Enum { + function fromEnum(x: a) returns (word) ; +} + +enum Food { Curry, Beans, Other } +enum Food { Duplicate } + +impl Enum { + function fromEnum(x: Food) returns (word) { + match (x) { case Food.Curry { return 1; } case Food.Beans { return 2; } case Food.Other { return 3; } } + } +} + +contract Food { + function main() public returns (word) { + return Enum.fromEnum(Food.Beans); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Eq.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Eq.solc new file mode 100644 index 00000000..78101957 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Eq.solc @@ -0,0 +1,15 @@ +enum Bool { True, False } + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +trait Ord where a: Eq { + function lt(x: a, y: a) returns (Bool) ; +} + +impl Eq { + function eq(x: word, y: word) returns (Bool) { + match (primEqWord(x,y)) { case 0 { return Bool.False; } default { return Bool.True ; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Filter.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Filter.solc new file mode 100644 index 00000000..7cee465a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Filter.solc @@ -0,0 +1,38 @@ +enum List { Nil, Cons(a, List) } +enum Bool { False, True } + +function and(x: Bool, y: Bool) returns (Bool) { + match (x, y) { case (Bool.False, _) { return Bool.False; } case (Bool.True, z) { return z; } } +} + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +impl Eq { + function eq(x: Word, y: Word) returns (Bool) { + match (primEqWord(x,y)) { case 0 { return Bool.False ; } default { return Bool.True ; } } + } +} + + +function filter(f: function(Word) returns (Bool), xs: List) returns (List) { + match (xs) { case List.Nil { return List.Nil ; } case List.Cons(y, ys) { match (f(y)) { case Bool.False { return filter(f,ys); } case Bool.True { return List.Cons(y,filter(f,ys)); } } } } +} + +function list1() returns (List) { + return List.Cons(1, List.Cons(2, List.Cons(3, List.Nil))); +} + +function foo0(y: Word) returns (List) { + return filter((lam (x) { return eq(x,y); }), list1()); +} + +function foo1() returns (List) { + return filter((lam (x) { return eq(x,1); }), list1()); +} + +function foo2(p: function(Word) returns (Bool), q: function(Word) returns (Bool)) returns (List) { + return filter(lam (x) { return and(p(x), q(x)) ; } + , list1()); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GetSet.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GetSet.solc new file mode 100644 index 00000000..be4cb88b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GetSet.solc @@ -0,0 +1,11 @@ +contract GetSet { + value : Word; + + function setValue(x) public { + value = x ; + } + + function getValue() public { + return value ; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GoodInstance.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GoodInstance.solc new file mode 100644 index 00000000..cbf61d8d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/GoodInstance.solc @@ -0,0 +1,24 @@ +trait Enum { + function fromEnum(x: a) returns (Word) ; +} + + enum Color { R, G, B } + +impl Enum { + function fromEnum(c: Color) returns (Word) { + match (c) { case Color.R { return 1; } case Color.G { return 2; } case Color.B { return 3; } } + } +} + + +enum Bool { False, True } + +impl Enum { + function fromEnum(b: Bool) returns (Word) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } + } +} + +contract GoodInstance { + function main() public returns (Word) { return fromEnum(Bool.True);} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/IncompleteInstDef.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/IncompleteInstDef.solc new file mode 100644 index 00000000..8db8de8c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/IncompleteInstDef.solc @@ -0,0 +1,14 @@ +trait Foo { + function foo(x: a, y: b) returns (b) ; + function faa(y: a) returns (a) ; +} + +enum Bool { False, True } + +enum Maybe { Nothing, Just(a) } +// missing the definition of Foo.foo +impl Foo { + function faa(y: Bool) returns (Bool) { + return y ; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Invokable.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Invokable.solc new file mode 100644 index 00000000..da38d23b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Invokable.solc @@ -0,0 +1,16 @@ + +trait invokable { + function invoke(s: self, a: args) returns (ret) ; + } + + function id(x: a) returns (a) { + return x ; + } + + enum IdToken { IdToken } + +impl invokable, a, a> { + function invoke(token: IdToken, a) returns (a) { + return id(a); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/KindTest.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/KindTest.solc new file mode 100644 index 00000000..69772cf5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/KindTest.solc @@ -0,0 +1,5 @@ +enum M { M } +function foo(x: M) {} + +enum P { P } +function foo2(x: P) {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch1.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch1.solc new file mode 100644 index 00000000..155603d8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch1.solc @@ -0,0 +1,6 @@ +enum Pair { Pair(a, b) } + +function foo(p: a) returns (word) { + let x: word = p; + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch2.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch2.solc new file mode 100644 index 00000000..99c6093c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/PairMatch2.solc @@ -0,0 +1,6 @@ + +function snd(p: (a, word)) returns (a) { + match (p) { case (_, w) { return w; } } +} + + diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Ref.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Ref.solc new file mode 100644 index 00000000..08c30297 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/Ref.solc @@ -0,0 +1,13 @@ +trait Ref { + function load(r: ref) returns (deref) ; + function store(r: ref, d: deref) returns (unit) ; +} + +enum Memory { new(a) } + +impl Ref, a> { + function load(r: Memory) returns (a) { + match (r) { case Memory.new(x) { return x; } } + } +} + diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SillyReturn.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SillyReturn.solc new file mode 100644 index 00000000..c4e7001d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SillyReturn.solc @@ -0,0 +1,6 @@ +enum Nat { Zero, Succ(Nat) } +enum Bool { True, False } + +function even(n) returns (Bool) { + match (n) { case Nat.Zero { return 1; return Bool.True; } case Nat.Succ(m) { return 0; return Bool.False; } } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SimpleInvoke.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SimpleInvoke.solc new file mode 100644 index 00000000..894b89dd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/SimpleInvoke.solc @@ -0,0 +1,18 @@ +function lambdaimpl1(x) { + return x; +} +enum LambdaTy0 { LambdaTy0 } +trait invokable { + function invoke(self: self, args: args) returns (ret) ; +} +impl invokable, a, a> { + function invoke(self: LambdaTy0, args: a) returns (a) { + return lambdaimpl1(args); + } +} +contract SimpleLambda { + function f() public { + let n = LambdaTy0.LambdaTy0 ; + return invokable.invoke(n, 0); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/StructMembers.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/StructMembers.solc new file mode 100644 index 00000000..cb418783 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/StructMembers.solc @@ -0,0 +1,134 @@ +/// Other used stdlib classes and types: +trait Ref { + function load(x: self) returns (deref) ; +} + +enum Uint256 { Uint256(Word) } +enum Bool { True, False } +enum Bytes32 { Bytes32(Word) } +enum Unit { Unit } + +enum Proxy { Proxy } +enum Memory { Memory(Word) } + +/// Specific new stdlib classes and types: + +trait StructMember {} +enum StructMember { StructMember } + +// "dead" is only here to compensate for non-relaxed coverage condition and +// incorrectly implemented Paterson condition +enum MemberAccess { MemberAccess(ty) } + + +/// Usage Example / Proof of Concept: + +/* + struct S { + x:Uint256; + y:Bool; + z:Bytes32; + } +*/ + +enum S { S(Pair>) } + +enum Field_x { FieldX } // Selector type for "x" +enum Field_y { FieldY } // Selector type for "y" +enum Field_z { FieldZ } // Selector type for "z" + +// StructMember instances for field selectors: +impl StructMember, Unit, Uint256> {} +impl StructMember, Uint256, Bool> {} +impl StructMember, Pair, Bytes32> {} + +/* Further compiler-internal builtin instances for use on stack (at least the stackref versions cannot be expressed in-language, + * but none of these rely on any layout other than the compiler-builtin stack layout, so we can handle these purely internally + * as "compiler magic"): + */ +/* + instance MemberAccess(S, Field_x):Ref(Uint256); + instance MemberAccess(stackref(S), Field_x):Ref(stackref(Uint256)); + instance MemberAccess(S, Field_y):Ref(Bool); + instance MemberAccess(stackref(S), Field_y):Ref(stackref(Bool)); + instance MemberAccess(S, Field_z):Ref(Bytes32); + instance MemberAccess(stackref(S), Field_z):Ref(stackref(Bytes32)); +*/ + + +/// Size of a type in memory +trait MemorySize { + function memorySize(x: Proxy) returns (Word) ; +} + +/// Size of the struct member types in memory: +impl MemorySize { function memorySize(x: Proxy) returns (Word) { return 0; } } +impl MemorySize { function memorySize(x: Proxy) returns (Word) { return 32; } } +impl MemorySize { function memorySize(x: Proxy) returns (Word) { return 32; } } +impl MemorySize { function memorySize(x: Proxy) returns (Word) { return 32; } } + +/// Memory size of pairs +impl MemorySize> { + function memorySize(x: Proxy<(a, b)>) returns (Word) { + let pa: Proxy; + let pb: Proxy; + let sz = memorySize(pa); + let szb = memorySize(pb); + assembly { sz := add(sz, szb) } // TODO: bounds check? + return sz; + } + +} + +/// Fragments of a generic memory implementation: +trait MemoryType { + function loadFromMemory(p: Proxy, off: Word) returns (self) ; +} + +impl MemoryType { + function loadFromMemory(p: Proxy, off: Word) returns (Uint256) { + let v; + assembly { v := mload(off) } + return Uint256.Uint256(v); + } +} + +impl Ref, a> where a: MemoryType { + function load(x: Memory) returns (a) { + let p: Proxy; + match (x) { case Memory.Memory(off) { return loadFromMemory(p, off); } } + } +} + +/// Crucial instance: member access to struct fields in memory: + +// Needs ridiculous amounts of constructor applications due to incorrect implementation of the Paterson Condition +// Needs to mention "ty" due to non-relaxed Coverage Condition +impl Ref, fieldType, Memory>, ty> where StructMember: StructMember, precedingTuple: MemorySize, Memory: Ref { + function load(x: MemberAccess, fieldType, Memory>) returns (ty) { + let ptr: Word; + match (x) { case MemberAccess.MemberAccess(Memory.Memory(y)) { ptr = y; } } + + let p: Proxy; + let offset = memorySize(p); + + assembly { ptr := add(ptr, offset) } + + let tyPtr: Memory = Memory.Memory(ptr); + return load(tyPtr); + } +} + +function test() returns () { + let x: Memory; + let memberAccess: MemberAccess, Field_x, Memory>; + memberAccess = MemberAccess.MemberAccess(x); + let result = load(memberAccess); + /* + Eventually, I imagine ``let result = x.x;`` to merely desugar to + + let result = load(MemberAccess(x):MemberAccess(_, Field_x)); + + which is equivalent to the above. + */ +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/add-moritz.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/add-moritz.solc new file mode 100644 index 00000000..4fb65b38 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/add-moritz.solc @@ -0,0 +1,48 @@ +function add(x: word, y: word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +trait Typedef { + function rep(x: self) returns (underlyingType) ; + function abs(x: underlyingType) returns (self) ; +} + +trait Add { + function add(x: a, y: a) returns (a) ; +} + +enum B { F, T } + + +impl Typedef { + function rep(x: B) returns (word) { + match (x) { case B.F { return 0; } case B.T { return 1; } } + } + + function abs(x: word) returns (B) { + match (x) { case 0 { return B.F; } case 1 { return B.T; } } + } +} + +impl Add { + function add(x: B, y: B) returns (B) { + match (x) { case B.F { match (y) { case B.F { return B.F; } case B.T { return B.T; } } } case B.T { match (y) { case B.F { return B.T; } case B.T { return B.F; } } } } + } +} + +function fun(a: (B, B), b: (B, B)) returns (B, B) { // -> c + match (a, b) { case ((a1, a2), (b1, b2)) { return (Add.add(a1, b1), fun(a2, b2)); } } + +} + +contract Compose { + + function main() public returns (word) { + let res = fun ((B.T, B.T, B.F), (B.F, B.F, B.T)); + match (res) { case (r1, r2, r3) { return Typedef.rep(r1); } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-no-return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-no-return.solc new file mode 100644 index 00000000..659cc666 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-no-return.solc @@ -0,0 +1,10 @@ +// mstore does not return a value, so it cannot be assigned. +contract Test { + function main() public { + let x: word; + assembly { + x := mstore(1, 1) + } + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-non-word.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-non-word.solc new file mode 100644 index 00000000..5442332f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-assign-non-word.solc @@ -0,0 +1,11 @@ +// An assembly assignment writes a raw scalar word, so its LHS must have type +// 'word'. Assigning to a non-word local (here a 'bool', whose runtime layout +// is a tagged inl/inr pair) would corrupt that layout, so the type checker +// must reject this program. +contract AsmBool { + function main() public returns (word) { + let b: bool = false; + assembly { b := add(1, 1) } + if (b) { return 1; } else { return 0; } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-let-no-return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-let-no-return.solc new file mode 100644 index 00000000..61b625cc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/asm-let-no-return.solc @@ -0,0 +1,8 @@ +// mstore does not return a value, so it cannot initialize a `let`. +contract Test { + function main() public { + assembly { + let x := mstore(1, 1) + } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-minimal.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-minimal.solc new file mode 100644 index 00000000..9f201dee --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-minimal.solc @@ -0,0 +1,12 @@ +// Minimal test for bound variable condition +// This SHOULD FAIL - variable 'bad' in context but not in instance head + + +trait TestBound {} +trait TestHelper {} + +enum TestType { TestType } + +// Variable 'bad' appears in context but not in instance head +// Should fail bound variable check +impl TestBound> where bad: TestHelper {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-only-test.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-only-test.solc new file mode 100644 index 00000000..94cd1505 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bound-only-test.solc @@ -0,0 +1,10 @@ +// Test only bound variable check, disable Patterson + +trait TestBound {} +trait TestHelper {} + +enum TestType { TestType } + +// Variable 'bad' appears in context but not in instance head +// Should fail bound variable check +impl TestBound> where bad: TestHelper {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bug-spec-generic-let.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bug-spec-generic-let.solc new file mode 100644 index 00000000..3e299347 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/bug-spec-generic-let.solc @@ -0,0 +1,43 @@ +// Bug: specStmt (Let i mty (Just e)) always called `atCurrentSubst i` AFTER +// `specExp`, causing `extSpSubst phi` (with original type-variable names) to +// corrupt subsequent let bindings. Concretely, `b_decoded : (uint256,uint256)` +// was mangled to `uint256` inside the ABIDecode instance for pairs. +// +// Root cause: when `ty'` is already concrete (freetv ty' == []), re-applying +// `atCurrentSubst` after `specExp` risks picking up unrelated bindings added +// by nested `specCall` invocations (e.g. {b -> uint256} from an inner decode). +// +// Fix: only re-apply when `freetv ty'` is non-empty (open type that needs +// resolution by the RHS, as in `let r : rep = Generic.from(x)`). +// +// Expected: compiles successfully. +// Actual (before fix): PANIC: Type mismatch expected uint256 actual (uint256,uint256) + +import std; +import std.dispatch; +import std.Generic; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +enum Pair { MkPair(uint256, uint256) } + +impl Generic { + function from(x: Pair) returns (uint256, uint256) { + match (x) { case Pair.MkPair(a, b) { return (a, b); } } + } + function to(x: (uint256, uint256)) returns (Pair) { + match (x) { case (a, b) { return Pair.MkPair(a, b); } } + } +} + +contract BugSpecGenericLet { + constructor() {} + + function roundtrip(a: uint256, b: uint256) returns (uint256) { + let p: Pair = Pair.MkPair(a, b); + let encoded: bytes memory = abi_encode(p); + let decoded: (uint256, uint256) = abi_decode(encoded, @(uint256, uint256), @MemoryWordReader); + match (decoded) { case (x, y) { match (and(Eq.eq(x, a), Eq.eq(y, b))) { case true { return uint256.uint256(1); } case false { return uint256.uint256(0); } } } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.snap new file mode 100644 index 00000000..74c32162 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.snap @@ -0,0 +1,15 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.solc +--- +error[SC0001]: parse error: unexpected `}` + --> /catenable-err.solc:3:1 + | +1 | trait Catenable { +2 | function cat(x:t) returns (bytes memory) +3 | } + | ^ unexpected token + | + = note: expecting `;`, or where clause + = note: while parsing function signature diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.solc new file mode 100644 index 00000000..429c795f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/catenable-err.solc @@ -0,0 +1,3 @@ +trait Catenable { + function cat(x:t) returns (bytes memory) +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-return-type-miss.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-return-type-miss.solc new file mode 100644 index 00000000..e52a1d4d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-return-type-miss.solc @@ -0,0 +1,9 @@ +enum bytes32 { bytes32(word) } + +trait Memory { + function encodeInto(v: t, target: word); +} + +impl Memory { + function encodeInto(v: bytes32, target: word) {} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-type-name-collision.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-type-name-collision.solc new file mode 100644 index 00000000..b11ab376 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/class-type-name-collision.solc @@ -0,0 +1,5 @@ +enum Foo { MkFoo } + +trait Foo { + function foo(x: a) returns (word) ; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/comp.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/comp.solc new file mode 100644 index 00000000..dac37fe8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/comp.solc @@ -0,0 +1,3 @@ +function compose(f, g, x) { + return f(g(x)); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/complexproxy.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/complexproxy.solc new file mode 100644 index 00000000..ad18d2f7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/complexproxy.solc @@ -0,0 +1,38 @@ +enum Proxy { Proxy } + +function add(x: word, y: word) {return x;} + +trait BaseMemoryType { + function memorySize(x: Proxy) returns (word) ; +} + + +impl BaseMemoryType { + function memorySize(x: Proxy) returns (word) { + return 32; + } +} + +impl BaseMemoryType<(a, b)> where a: BaseMemoryType, b: BaseMemoryType { + + function memorySize(x) returns (word) { // not correct semantically, just for debugging + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + return add(BaseMemoryType.memorySize(aProxy), + // BaseMemoryType.memorySize(Proxy:Proxy(b)) + morefun(bProxy) + ); + } +} +// this should trigger a type error. +function morefun(p: Proxy) returns (word) { + let proxy: Proxy = Proxy.Proxy; + return BaseMemoryType.memorySize(proxy); +} + +contract TestMemoryType { + function main() public returns (word) { + let proxy: Proxy<(word, word)> = Proxy.Proxy; + return BaseMemoryType.memorySize(proxy); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/compose_desugared.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/compose_desugared.solc new file mode 100644 index 00000000..bc309c3e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/compose_desugared.solc @@ -0,0 +1,35 @@ +function compose(f: d, g: e) returns (t_closure1) where d: invokable, e: invokable { + return t_closure1.t_closure1(f,g); +} + +enum t_closure1 { t_closure1(d, e) } + +function lambda2(c: t_closure1, x: a) returns (c) where d: invokable, e: invokable { + match (c) { case t_closure1.t_closure1(f, g) { return invokable.invoke(f, invokable.invoke(g,x)); } } + } + +impl invokable, a, c> where d: invokable, e: invokable { + function invoke(self: t_closure1, args: a) returns (c) { + return lambda2(self, args); + } +} + +enum t_id3 { t_id3 } + +function id(x: a) returns (a) { + return x; +} + +impl invokable, a, a> { + function invoke(self: t_id3, args: a) returns (a) { + match (self) { case t_id3.t_id3 { return id(args) ; } } + } +} + +contract Foo { + function main() public returns (word) { + let f = compose(t_id3.t_id3, t_id3.t_id3); + return invokable.invoke(f, 0); + } +} + diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/const-array.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/const-array.solc new file mode 100644 index 00000000..44e325a0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/const-array.solc @@ -0,0 +1,111 @@ + +enum Zero {} +enum Succ {} + +trait TAdd {} +impl TAdd<(Zero, a), a> {} +impl TAdd<(Succ, a), Succ> where (b, a): TAdd {} + +trait Eq {} +impl Eq {} + +// this should work but doesnt: forall sizel sizer elem sizeout . (sizel, sizer):TAdd(sizeout) +function concat(lhs: array memory, rhs: array memory) returns (array memory) where pairSizelSizer: Eq<(sizel, sizer)>, pairSizelSizer: TAdd { + let result: array memory = memory.memory(0); + return result; // :D +} + +enum Itself { ItselfRuntimeTag } + +enum array { array } +enum memory { memory(word) } + +trait IndexAccessible { + function set(self: self, ix: indexType, val: elementType); + function at(self: self, ix: indexType) returns (elementType); +} + +trait ToWord { + function toWord(self: Itself) returns (word); +} + +impl ToWord { + function toWord(zero: Itself) returns (word) { return 0; } +} + +impl ToWord> where prev: ToWord { + function toWord(self: Itself>) returns (word) { + let prevTag: Itself = Itself.ItselfRuntimeTag; + let returnVal: word = ToWord.toWord(prevTag); + assembly { + returnVal := add(1, returnVal) + } + return returnVal; + } +} + +trait MemoryType { + function load(ptr: word) returns (self); + function store(ptr: word, value: self); +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let val: word; + assembly { val := mload(ptr) } + return val; + } + function store(ptr: word, value: word) { + assembly { mstore(ptr, value) } + } +} + +impl IndexAccessible memory, word, elem> where size: ToWord, elem: MemoryType { + function at(self, index) returns (elem) { + let sizeTag: Itself = Itself.ItselfRuntimeTag; + let sizeValue = ToWord.toWord(sizeTag); + // this should work but doesn't + // assembly { + // if iszero(lt(index, sizeValue)) { + // revert(0, 0) + // } + //} + + match (self) { case memory.memory(offset) { let x = offset; // can't use this inside the assembly block :-( + assembly { + index := add(x, mul(32, index)) + } + return MemoryType.load(index); } } + } + + function set(self, index, val) { + let sizeTag: Itself = Itself.ItselfRuntimeTag; + let sizeValue = ToWord.toWord(sizeTag); + + //assembly { + // if iszero(lt(index, sizeValue)) { + // revert(0, 0) + // } + //} + + match (self) { case memory.memory(offset) { let x = offset; // can't use this inside the assembly block :-( + assembly { + index := add(x, mul(32, index)) + } + MemoryType.store(index, val); } } + } +} + + + +contract Array { + + function main() public { + let arr: array>>>, word> memory = memory.memory(42); // = (1,2,3,4,5,6,7,8,9,10); + IndexAccessible.set(arr, 4, 33); + + // this (correctly) typechecks but doesn't specialize + let res = concat(arr, arr); // this typechecks + return IndexAccessible.at(arr, 4); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-inst.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-inst.solc new file mode 100644 index 00000000..52a83c53 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-inst.solc @@ -0,0 +1,20 @@ +trait Test { function f(x: self); } + +default impl Test { function f(x: self) {}} + +enum memory { memory(word) } +enum Proxy { Proxy } + +impl Test>> { function f(x: self) {}} + +function f(p: Proxy) { + let x: a memory; + Test.f(x); +} + +function g() { + let nestedMemoryProxy: Proxy>> = Proxy.Proxy; + f(nestedMemoryProxy); // needs to choose default instance in Test.f + let memoryProxy: Proxy> = Proxy.Proxy; + f(memoryProxy); // needs to choose concrete instance +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-missing.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-missing.solc new file mode 100644 index 00000000..aa56e4df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-missing.solc @@ -0,0 +1,18 @@ +trait Test { function f(x: self); } + +enum memory { memory(word) } +enum Proxy { Proxy } + +impl Test>> { function f(x: self) {}} + +function f(p: Proxy) { + let x: a memory; + Test.f(x); +} + +function g() { + let nestedMemoryProxy: Proxy>> = Proxy.Proxy; + f(nestedMemoryProxy); // needs to choose default instance in Test.f + let memoryProxy: Proxy> = Proxy.Proxy; + f(memoryProxy); // needs to choose concrete instance +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-weak.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-weak.solc new file mode 100644 index 00000000..4bceb395 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/default-instance-weak.solc @@ -0,0 +1,26 @@ +trait Test { function f(x: self) returns (weak) ; } + +enum memory { memory(word) } +enum Proxy { Proxy } +enum Bool { True, False } +default impl Test { function f(x: a) returns (word) { return 42; }} + +impl Test>, Bool> { function f(x: memory>) returns (Bool) { return Bool.True; }} + +// If we choose the default instance to typecheck f, +// this will pass type-checking, since ``r`` is word. +// But: for a = memory(word), ``r`` will be ``bool`` and this is invalid! +function f(p: Proxy) { + let x: a memory; + let r: word = Test.f(x); + assembly { + sstore(0, r) + } +} + +function g() { + let nestedMemoryProxy: Proxy>> = Proxy.Proxy; + f(nestedMemoryProxy); // valid, since default instance is used + let memoryProxy: Proxy> = Proxy.Proxy; + f(memoryProxy); // PROBLEM: now we have a bool cross the assembly barrier +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.snap new file mode 100644 index 00000000..1a0876e5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.snap @@ -0,0 +1,13 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.solc +--- +error[SC0001]: invalid token `#` + --> /derive-self-return-poc.solc:35:1 + | +34 | +35 | #[derive(CloneLike)] + | ^ invalid token +36 | enum Box { Box(a) } + | diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.solc new file mode 100644 index 00000000..5167cc0d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/derive-self-return-poc.solc @@ -0,0 +1,40 @@ +// PoC: #[derive(...)] currently accepts a single-parameter class whose method +// returns the self type, but the generated method only applies Generic.from to +// arguments and does not wrap the result back with Generic.to. +// +// Expected behavior: +// either reject this derive with a clear diagnostic, or generate: +// return Generic.to(CloneLike.clone(Generic.from(x))); +// +// Current behavior: +// generated clone(x : Box(a)) -> Box(a) returns the representation type, +// causing type inference to fail with an infinite type error. + +import std; +import std.Generic; + +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +trait CloneLike { + function clone(x: a) returns (a) ; +} + +impl CloneLike { + function clone(x: word) returns (word) { return x; } +} + +impl CloneLike<()> { + function clone(x: ()) returns () { return (); } +} + +impl CloneLike<(f, g)> where f: CloneLike, g: CloneLike { + function clone(x: (f, g)) returns (f, g) { return x; } +} + +#[derive(CloneLike)] +enum Box { Box(a) } + +function cloneBox(x: Box) returns (Box) { + return CloneLike.clone(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dispatch.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dispatch.solc new file mode 100644 index 00000000..85a4e894 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dispatch.solc @@ -0,0 +1,253 @@ + +// --- Preliminaries --- + +enum Bool { True, False } +enum Proxy { Proxy } + +// --- Core Data Types --- + +// A contract contains a tuple of methods and a single fallback +// TODO: implement receive() +enum Contract { Contract(methods, fb) } + +// A method contains an implementation (fn) as well as it's name and type signature +enum Method { Method(name, args, rets, fn) } + +// Contains the implementation for the fallback (fn) as well as it's type signature +enum Fallback { Fallback(args, rets, fn) } + +// --- Method Selectors --- + +// For each method in a contract the compiler generates a unique type and +// produces a `Selector` instance for that type that returns the selector hash +trait Selector { + function hash(prx: Proxy) returns (word) ; +} + +// Method has a Selector if its name has a Selector +impl Selector> where name: Selector { + function hash(prx: Proxy>) returns (word) { + let nameProxy: Proxy = Proxy.Proxy; + return Selector.hash(nameProxy); + } +} + +// --- Method Execution --- + +// Describes how to execute a given method / fallback +trait ExecMethod { + function exec(x: ty, pstatus: Proxy) returns () ; +} + +// If fn matches the provided args/ret types, then we can execute any method +impl ExecMethod, Proxy, fn>> where fn: invokable { + function exec(m: Method, pstatus: Proxy) returns () { + match (m) { case Method.Method(nm, args, rets, fn) { // check callvalue + let methodProxy: Proxy> = Proxy.Proxy; + MethodLevelCallvalueCheck.checkCallvalue(methodProxy, pstatus); + + // check we have enough calldata for the head of args + // abi decode args from calldata + // call fn with args + // abi encode rets to memory + // returndata copy encoded returns + // evm return + return (); } } + } +} + +// If fn matches the provided args/ret types, then we can execute any fallback +impl ExecMethod, Proxy, fn>> where fn: invokable { + function exec(fb: Fallback, pstatus: Proxy) returns () { + match (fb) { case Fallback.Fallback(args, rets, fn) { // check callvalue + let fallbackProxy: Proxy> = Proxy.Proxy; + MethodLevelCallvalueCheck.checkCallvalue(fallbackProxy, pstatus); + + // check we have enough calldata for the head of args + // abi decode args from calldata + // call fn with args + // abi encode rets to memory + // returndata copy encoded returns + // evm return + return (); } } + } +} + +// --- Method Dispatch --- + +// For a given tuple of methods this executes the method specified by the first four bytes of calldata +trait RunDispatch { + function go(methods: ty, pstatus: Proxy) returns () ; +} + +// We can dispatch to a single executable method with a known selector +// TODO: do we need this instance? +impl RunDispatch where m: ExecMethod, m: Selector { + function go(method: m, pstatus: Proxy) returns () { + let methodProxy: Proxy = Proxy.Proxy; + match (selector_matches(methodProxy)) { case Bool.True { ExecMethod.exec(method, pstatus); } case Bool.False { return (); } } + } +} + +// We can dispatch to a tuple of executable methods with a known selector +impl RunDispatch<(n, m)> where n: ExecMethod, n: Selector, m: ExecMethod, m: Selector { + function go(methods: (n, m), pstatus: Proxy) returns () { + let firstMethodProxy: Proxy = Proxy.Proxy; + let secondMethodProxy: Proxy = Proxy.Proxy; + match (methods) { case (method_n, method_m) { match (selector_matches(firstMethodProxy)) { case Bool.True { ExecMethod.exec(method_n); } case Bool.False { match (selector_matches(secondMethodProxy)) { case Bool.True { ExecMethod.exec(method_m, pstatus); } case Bool.False { return (); } } } } } } + } +} + +// Recursive instance +impl RunDispatch<(n, m)> where n: ExecMethod, n: Selector, m: RunDispatch { + function go(methods: (n, m), pstatus: Proxy) returns () { + let methodProxy: Proxy = Proxy.Proxy; + match (methods) { case (method_n, rest) { match (selector_matches(methodProxy)) { case Bool.True { ExecMethod.exec(method_n, pstatus); } case Bool.False { RunDispatch.go(rest, pstatus); } } } } + } +} + +// TODO: we only wanna do the calldataload once +// Given evidence of a name with a known selector, we can check if it matches the selector in the first four bytes of calldata +function selector_matches(prx: Proxy) returns (Bool) where name: Selector { + let hash = Selector.hash(prx); + let res: word; + assembly { + let sel := shr(224, calldataload(0)) + res := eq(sel, hash) + } + match (res) { case 0 { return Bool.False; } default { return Bool.True; } } +} + +// --- Callvalue Checks --- + +// If every method on a contract is non payable, we lift the callvalue check to run before method dispatch +// NonPayable instances should be generated by the compiler as part of desugaring +trait NonPayable {} +trait AllNonPayable {} +impl AllNonPayable<(n, m)> where n: NonPayable, m: AllNonPayable {} + + +enum CallvalueChecked {} + +enum CallvalueUnchecked {} +trait MethodsMustCheckCalldata {} +impl MethodsMustCheckCalldata {} + +// If every method is non payable we run the callvalue check before method dispatch +trait TopLevelCallvalueCheck { + function checkCallvalue(prx: Proxy) returns (Proxy) ; +} + +default impl TopLevelCallvalueCheck { + function checkCallvalue(prx: Proxy) returns (Proxy) { + let result: Proxy = Proxy.Proxy; + return result; + } +} + +impl TopLevelCallvalueCheck where methods: AllNonPayable { + function checkCallvalue(prx: Proxy) returns (Proxy) { + assembly { + if gt(callvalue(), 0) { + mstore(0,0x2) + revert(0,32) + } + } + let result: Proxy = Proxy.Proxy; + return result; + } +} + +// If only some methods are non payable, then we run the check during method execution +trait MethodLevelCallvalueCheck { + function checkCallvalue(pty: Proxy, pstatus: Proxy) returns () ; +} + +default impl MethodLevelCallvalueCheck { + function checkCallvalue(pty: Proxy, pstatus: Proxy) returns () { } +} + +impl MethodLevelCallvalueCheck where method: NonPayable, status: MethodsMustCheckCalldata { + function checkCallvalue(pty: Proxy, pstatus: Proxy) returns () { + assembly { + if gt(callvalue(), 0) { + mstore(0, 0x1) + revert(0, 32) + } + } + } +} + +// --- Contract Execution --- + +// Describes how to execute a given contract +trait RunContract { + function exec(v: c) returns () ; +} + +// If we have a dispatch for the contracts methods, and we know how to execute it's fallback, then we can define an entrypoint +impl RunContract> where methods: RunDispatch, fb: ExecMethod { + function exec(c: Contract) returns () { + match (c) { case Contract.Contract(ms, fb) { // set free memory pointer to the output of memoryguard + // https://docs.soliditylang.org/en/v0.8.30/yul.html#memoryguard + // TODO: we will need to consider immutables here at some point... + // assembly { mstore(0x40, memoryguard(128)) } + + // if all methods are non payable then check callvalue + let contractProxy: Proxy<(fb, methods)> = Proxy.Proxy; + let callvalueChecked = TopLevelCallvalueCheck.checkCallvalue(contractProxy); + + // check that we have at least 4 bytes of calldata + let haveSelector: word; + assembly { + haveSelector := lt(3, calldatasize()) + } + + match (haveSelector) { case 0 { assembly { revert(0,0) } } default { // dispatch to method based on selector + RunDispatch.go(ms, callvalueChecked); + // run fallback if no methods matched + ExecMethod.exec(fb); } } } } + } +} + +// --- Manually Desugared Example --- + +// compiler generated + +function revert_handler() returns () { + assembly { revert(0,0) } +} + +enum C_Add2_Selector { C_Add2_Selector } + +impl Selector { + function hash(prx: Proxy) returns (word) { + // This would be keccak256("add2(uint256,uint256)") >> 224 + // Compiler computes this at compile time + return 0x29fcda33; // placeholder value + } +} + +// transform + +contract C { + function add2(x: word, y: word) public returns (word) { + let ret: word; + assembly { ret := add(x,y) } + return ret; + } + + function main() public returns (word) { + let methodArgsProxy: Proxy<(word, word)> = Proxy.Proxy; + let methodReturnProxy: Proxy = Proxy.Proxy; + let fallbackArgsProxy: Proxy<()> = Proxy.Proxy; + let fallbackReturnProxy: Proxy<()> = Proxy.Proxy; + let c = Contract.Contract( + Method.Method(C_Add2_Selector.C_Add2_Selector, methodArgsProxy, methodReturnProxy, add2), + Fallback.Fallback(fallbackArgsProxy, fallbackReturnProxy, revert_handler) + ); + + RunContract.exec(c); + return 0; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-no-context-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-no-context-fail.solc new file mode 100644 index 00000000..9825d4a3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-no-context-fail.solc @@ -0,0 +1,8 @@ +enum Option { None, Some(word) } + +function bad() returns (Option) { + let x = Some(1); + return x; +} + +// migrate-syntax: keep-unqualified-constructor diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-unknown-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-unknown-fail.solc new file mode 100644 index 00000000..462d42ad --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/dot-expression-unknown-fail.solc @@ -0,0 +1,5 @@ +enum Option { None, Some(word) } + +function bad() returns (Option) { + return Option.Nope(1); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-contract-name.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-contract-name.solc new file mode 100644 index 00000000..200dfd98 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-contract-name.solc @@ -0,0 +1,3 @@ +contract Foo {} + +contract Foo {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-type-name.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-type-name.solc new file mode 100644 index 00000000..c5164fba --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/duplicated-type-name.solc @@ -0,0 +1,6 @@ +enum Foo { Bar } +enum Foo { Baz } + +function main() { + let x = Foo.Baz; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.snap new file mode 100644 index 00000000..27c825a0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.snap @@ -0,0 +1,25 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.solc +--- +error[SC0001]: fallback function must not declare input parameters + --> /fallback-with-args.solc:7:13 + | +6 | +7 | fallback(x: uint256) returns () { + | ^^^^^^^^^^^^ +8 | revert("fallback-was-called"); + | + = note: while parsing fallback definition +--- + +error[SC0001]: parse error: unexpected `)` + --> /fallback-with-args.solc:8:37 + | +7 | fallback(x: uint256) returns () { +8 | revert("fallback-was-called"); + | ^ unexpected token +9 | } + | + = note: expecting `%=`, `&&`, `&=`, `&`, `(`, `*`, `+=`, `-=`, `.`, `;`, `<`, `=`, `>`, `?`, `[`, `^=`, `^`, `as`, `|=`, `|`, or `||` diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.solc new file mode 100644 index 00000000..f1172f52 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-args.solc @@ -0,0 +1,10 @@ +import std; // migrate-syntax: keep-legacy-negative +import std.dispatch; + +contract BadFallback { + constructor() {} + + fallback(x: uint256) returns () { + revert("fallback-was-called"); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.snap new file mode 100644 index 00000000..a83a969d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.snap @@ -0,0 +1,14 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.solc +--- +error[SC0001]: fallback function must return unit (`()`) + --> /fallback-with-return.solc:7:25 + | +6 | +7 | fallback() returns (uint256) { + | ^^^^^^^ +8 | return uint256.uint256(0); + | + = note: while parsing fallback definition diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.solc new file mode 100644 index 00000000..6f59c91c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/fallback-with-return.solc @@ -0,0 +1,10 @@ +import std; +import std.dispatch; + +contract BadFallback { + constructor() {} + + fallback() returns (uint256) { + return uint256.uint256(0); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/field-access.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/field-access.solc new file mode 100644 index 00000000..f3f343bd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/field-access.solc @@ -0,0 +1,18 @@ +import std; + +contract PoC { + field : word; + + function set_x(b: bool) public returns (bool) { + field = b; // BUG: `word` shouldn't be unified with `bool`. + return b; + } + + function init(foo: bool) public returns () { + field = 2; + } + + function main() public returns () { + + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/for-let-post.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/for-let-post.solc new file mode 100644 index 00000000..f8462ad7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/for-let-post.solc @@ -0,0 +1,10 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract C { + function main() public returns (word) { + let i: word = 0; + let s: word = 99; + for(i=0;i<=0;let j=1) { s = j; i = i + 1; } + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-manual-no-pragma.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-manual-no-pragma.solc new file mode 100644 index 00000000..67bcedb9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-manual-no-pragma.solc @@ -0,0 +1,18 @@ +// Error case: manual Generic instance without pragma no-generic-instance-for. +// The compiler must reject this with a conflict error. + +import std.Generic; + +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +enum Foo { MkFoo(word) } + +impl Generic { + function from(x: Foo) returns (word) { + match (x) { case Foo.MkFoo(v) { return v; } } + } + function to(v: word) returns (Foo) { + return Foo.MkFoo(v); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-product-no-pragma.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-product-no-pragma.solc new file mode 100644 index 00000000..b74da834 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-product-no-pragma.solc @@ -0,0 +1,21 @@ +import std; +import std.dispatch; +import std.Generic; +import std.ABIGeneric; + +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +enum Point { Point(uint256, uint256) } + +// Manual Generic instance without pragma no-generic-instance-for Point. +// The compiler must reject this with a conflict error. +impl Generic { + function from(p: Point) returns (uint256, uint256) { + match (p) { case Point.Point(x, y) { return (x, y); } } + } + function to(t: (uint256, uint256)) returns (Point) { + match (t) { case (x, y) { return Point.Point(x, y); } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-sum-no-pragma.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-sum-no-pragma.solc new file mode 100644 index 00000000..939e082c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/generic-sum-no-pragma.solc @@ -0,0 +1,21 @@ +import std; +import std.dispatch; +import std.Generic; +import std.ABIGeneric; + +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +enum Option { None, Some(a) } + +// Manual Generic instance without pragma no-generic-instance-for Option. +// The compiler must reject this with a conflict error. +impl Generic, sum<(), uint256>> { + function from(x: Option) returns (sum<(), uint256>) { + match (x) { case Option.None { return inl(()); } case Option.Some(v) { return inr(v); } } + } + function to(r: sum<(), uint256>) returns (Option) { + match (r) { case inl(_) { return Option.None; } case inr(v) { return Option.Some(v); } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/index-example.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/index-example.solc new file mode 100644 index 00000000..3362dbfd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/index-example.solc @@ -0,0 +1,71 @@ +enum storage { storage(word) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +enum mapping { mapping(word, Proxy, Proxy) } // storage by default +// data mapRef(a) = mapRef(word); //ref to a map elem + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign, a> { + function assign(l: storageRef, y: a) { + } +} + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + +impl LValueMemberAccess, storageRef> where StructField: CStructField { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + return storageRef.storageRef(0x100); + } +} + +// ------------------------------------------------------------------ +// Indexed access +// ------------------------------------------------------------------ + +enum mapping { mapping(word) } +enum IndexAccessProxy { IndexAccessProxy(map, index) } +enum IndexAccessProxy2 { IndexAccessProxy2(map, index, Proxy) } + +impl LValueMemberAccess member)>, index, member>, storageRef> { + function memberAccess(x: IndexAccessProxy, index, member>) returns (storageRef) { + return storageRef.storageRef(0); + } +} + +enum MintCtx { MintCtx } +enum balances_sel { balances_sel } +impl CStructField, mapping(word => word), ()> {} + + function mint(amount: word) { + let bal_prx = MemberAccessProxy.MemberAccessProxy(MintCtx.MintCtx, balances_sel.balances_sel); + let bal_ref = LValueMemberAccess.memberAccess(bal_prx); + + Assign.assign( + LValueMemberAccess.memberAccess( + IndexAccessProxy.IndexAccessProxy( + // bal_ref // this works, but inlining bal_ref leads to error + LValueMemberAccess.memberAccess(bal_prx) + , 0 + ) + ) + , amount + ) ; + + } diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-closure-error-invalid-member.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-closure-error-invalid-member.solc new file mode 100644 index 00000000..538a8f83 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-closure-error-invalid-member.solc @@ -0,0 +1,11 @@ +trait CtFun { + function ct(x: t) returns (function(t) returns (t)) ; +} + +impl CtFun { + function ct(x: word) returns (function(word) returns (word)) { + return lam (y: bool) { + return x; + }; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-context-wrong-kind.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-context-wrong-kind.solc new file mode 100644 index 00000000..a8e6a0ed --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-context-wrong-kind.solc @@ -0,0 +1,5 @@ +trait Foo {} + +trait C {} + +impl C<(word, t)> where t: Foo {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-wrong-sig.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-wrong-sig.solc new file mode 100644 index 00000000..7623901a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/instance-wrong-sig.solc @@ -0,0 +1,15 @@ +enum uint256 { uint256(word) } +enum Proxy { Proxy } +trait ABIAttribs { + function headSize(ty: Proxy) returns (word) ; + function isStatic(ty: Proxy) returns (bool) ; +} + +impl ABIAttribs<()> { + function headSize(ty: Proxy) returns (word) { return 0; } + function isStatic(ty: Proxy) returns (bool) { return true; } +} +impl ABIAttribs { + function headSize(ty: Proxy) returns (word) { return 32; } + function isStatic(ty: Proxy) returns (bool) { return true; } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/joinErr.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/joinErr.solc new file mode 100644 index 00000000..5ee311bd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/joinErr.solc @@ -0,0 +1,19 @@ +contract Option { + enum Option { None, Some(a) } + enum Bool { False, True } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + let result = Option.None; + match (mmx) { case Option.Some(Option.Some(x)) { result = Option.Some(x); } case Option.None { result = Option.None; } } + return result; + } + + + function main() public returns (word) { + return maybe(0, join(Option.Some(Option.Some(Bool.False)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/listeq.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/listeq.solc new file mode 100644 index 00000000..536cce44 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/listeq.solc @@ -0,0 +1,10 @@ +enum List { Nil, Cons(a, List) } +enum Bool { False, True } + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +function foo() { + return Eq.eq(List.Nil, List.Nil); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/mainproxy.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/mainproxy.solc new file mode 100644 index 00000000..173f6ffc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/mainproxy.solc @@ -0,0 +1,25 @@ +enum Proxy { Proxy } + +trait BaseMemoryType { + function memorySize(x: Proxy) returns (word) ; +} + + +impl BaseMemoryType { + function memorySize(x: Proxy) returns (word) { + return 32; + } +} + + +function morefun(p: Proxy) returns (word) { + let proxy: Proxy = Proxy.Proxy; + return BaseMemoryType.memorySize(proxy); +} + +contract TestMemoryType { + function main() public returns (word) { + let proxy: Proxy = Proxy.Proxy; + return morefun(proxy); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/match-compiler-undef-asm.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/match-compiler-undef-asm.solc new file mode 100644 index 00000000..2cf53adf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/match-compiler-undef-asm.solc @@ -0,0 +1,16 @@ +enum Foo { Foo(word) } + +function read(x: Foo) returns (word) { + let res: word; + match (x) { case Foo.Foo(w) { assembly { + res := w + } } } + return res; +} + +contract Bla { + + function main() public returns (word) { + return read(Foo.Foo(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/missing-instance.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/missing-instance.solc new file mode 100644 index 00000000..1e4c9248 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/missing-instance.solc @@ -0,0 +1,27 @@ +// Note: this class has no instances! +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + +trait MemoryType { + function load(ptr: word) returns (self) ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let loaded: word = MemoryType.load(ptr); + return Typedef.abs(loaded); + // `abs` does not make sense here, but it triggers the bug: + // the typechecker should complain about missing instance here + } +} + +contract C { + function main() public returns (word) { + let ptr: word = 0; + // if we inline the let below into return then another bug occurs: main is typed as forall a. () -> a + // let w:word = MemoryType.load(0); + return MemoryType.load(0); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/nano-desugared.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/nano-desugared.solc new file mode 100644 index 00000000..684f7541 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/nano-desugared.solc @@ -0,0 +1,403 @@ +function addW(x: word, y: word) { + let res: word; + assembly { res := add(x, y) + } + return res; +} +function subW(x: word, y: word) { + let res: word; + assembly { res := sub(x, y) + } + return res; +} +function addU(x: uint, y: uint) returns (uint) { + let res: word; + let xw: word = Num.toWord(x) ; + let yw: word = Num.toWord(y) ; + assembly { res := add(xw, yw) + } + return uint.uint(res); +} +function hash1(x: word) returns (word) { + let result: word = 0 ; + assembly { mstore(0, x) + result := keccak256(0, 32) + } + return result; +} +function hash2(x: word, y: word) returns (word) { + let result: word = 0 ; + assembly { mstore(0, x) + mstore(32, y) + result := keccak256(0, 64) + } + return result; +} +enum Bool { False, True } +function not(b: Bool) returns (Bool) { + match (b) { case Bool.False { return Bool.True; } case Bool.True { return Bool.False; } } +} +function or(x: Bool, y: Bool) returns (Bool) { + match (x) { case Bool.False { return y; } case Bool.True { return Bool.True; } } +} +function fromBool(b) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } +} +function toBool(x: word) { + match (x) { case 0 { return Bool.False; } default { return Bool.True; } } +} +trait Num { + function toWord(x: a) returns (word) ; + function fromWord(x: word) returns (a) ; + function add(x: a, y: a) returns (a) ; + function sub(x: a, y: a) returns (a) ; + function eq(x: a, y: a) returns (Bool) ; + function gt(x: a, y: a) returns (Bool) ; +} +impl Num { + function toWord(x: word) returns (word) { + return x; + } + function fromWord(x: word) returns (word) { + return x; + } + function add(x: word, y: word) returns (word) { + return addW(x, y); + } + function sub(x: word, y: word) returns (word) { + return addW(x, y); + } + function eq(x: word, y: word) returns (Bool) { + let res: word; + assembly { res := eq(x, y) + } + return toBool(res); + } + function gt(x: word, y: word) returns (Bool) { + let res: word; + assembly { res := gt(x, y) + } + return toBool(res); + } +} +function ge(x: a, y: a) returns (Bool) where a: Num { + return or(Num.gt(x, y), Num.eq(x, y)); +} +enum uint { uint(word) } +impl Num { + function toWord(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + + function fromWord(x: word) returns (uint) { + return uint.uint(x); + } + function add(x: uint, y: uint) returns (uint) { + return uint.uint(addW(Num.toWord(x), Num.toWord(y))); + } + function sub(x: uint, y: uint) returns (uint) { + return uint.uint(subW(Num.toWord(x), Num.toWord(y))); + } + function eq(x: uint, y: uint) returns (Bool) { + return Num.eq(Num.toWord(x), Num.toWord(y)); + } + function gt(x: uint, y: uint) returns (Bool) { + return Num.gt(Num.toWord(x), Num.toWord(y)); + } +} +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} +impl Typedef { + function rep(x: word) returns (word) { + return x; + } + function abs(x: word) returns (word) { + return x; + } +} +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} +enum address { address(word) } +impl Typedef { + function rep(x: address) returns (word) { + match (x) { case address.address(y) { return y; } } + } + function abs(x: word) returns (address) { + return address.address(x); + } +} +enum storage { storage(word) } +enum ContractStorage { ContractStorage(cxt) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } +enum mapping { mapping(word, Proxy, Proxy) } +enum mapRef { mapRef(word) } +impl Typedef { + function rep(x: a storage) returns (word) { + match (x) { case storage.storage(y) { return y; } } + } + function abs(x: word) returns (a storage) { + return storage.storage(x); + } +} +impl Typedef, word> { + function rep(x: storageRef) returns (word) { + match (x) { case storageRef.storageRef(y) { return y; } } + } + function abs(x: word) returns (storageRef) { + return storageRef.storageRef(x); + } +} +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} +enum ref { ref(a) } +impl Assign, a> { + function assign(l: ref, r: a) returns () { + return (); + } +} +trait StorageType { + function sload(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} +trait StorageSize { + function size(x: Proxy) returns (word) ; +} +function sload_(x: word) returns (word) { + let res: word; + assembly { res := sload(x) + } + return res; +} +function sstore_(a: word, v: word) { + assembly { sstore(a, v) + } +} +impl StorageType { + function sload(ptr: word) returns (word) { + let r: word; + assembly { r := sload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { sstore(ptr, value) + } + } +} +impl StorageType { + function sload(ptr: word) returns (uint) { + let loaded: uint = Typedef.abs(sload_(ptr)); + return loaded; + } + function store(ptr: word, value: uint) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} +impl StorageType
{ + function sload(ptr: word) returns (address) { + let loaded: address = Typedef.abs(sload_(ptr)); + return loaded; + } + function store(ptr: word, value: address) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} +impl Assign, a> where a: StorageType { + function assign(l: storageRef, y: a) returns () { + StorageType.store(Typedef.rep(l), y); + } +} +trait CStructField { +} +enum StructField { StructField(structType) } +enum MemberAccessProxy { MemberAccessProxy(a, field) } +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} +impl LValueMemberAccess, storageRef> where StructField: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)) ; + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = StorageSize.size(offsetProxy) ; + assembly { ptr := add(ptr, size) + } + return storageRef.storageRef(ptr); + } +} +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} +impl StorageSize
{ + function size(x: Proxy
) returns (word) { + return 1; + } +} +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy) ; + let bProxy: Proxy = Proxy.Proxy; + let b_sz: word = StorageSize.size(bProxy) ; + assembly { a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} +impl LValueMemberAccess, fieldSelector, offsetType>, storageRef> where StructField, fieldSelector>: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (storageRef) { + let ptr: word = 256 ; + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy) ; + assembly { ptr := add(ptr, offsetSize) + } + return storageRef.storageRef(ptr); + } +} +impl RValueMemberAccess, fieldSelector, offsetType>, fieldType> where StructField, fieldSelector>: CStructField, fieldType: StorageType, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (fieldType) { + let ptr: word = 256 ; + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy) ; + let loaded: fieldType = StorageType.sload(addW(ptr, offsetSize)); + return loaded; + } +} +enum mapping { mapping(word) } +impl Typedef member), word> { + function rep(x: mapping(index => member)) returns (word) { + match (x) { case mapping.mapping(y) { return y; } } + } + function abs(x: word) returns (mapping(index => member)) { + return mapping.mapping(x); + } +} +impl StorageSize member)> { + function size(x: Proxy member)>) returns (word) { + return 1; + } +} +enum IndexAccessProxy { IndexAccessProxy(map, index) } +impl LValueMemberAccess member)>, index, member>, storageRef> where index: Typedef { + function memberAccess(x: IndexAccessProxy member)>, index, member>) returns (storageRef) { + return storageRef.storageRef(indexStorageSlot(x)); + } +} +impl RValueMemberAccess, member> where index: Typedef, member: StorageType, map: Typedef { + function memberAccess(x: IndexAccessProxy) returns (member) { + let slot: word = indexStorageSlot(x) ; + return StorageType.sload(slot); + } +} +function indexStorageSlot(x: IndexAccessProxy) returns (word) where map: Typedef, index: Typedef { + match (x) { case IndexAccessProxy.IndexAccessProxy(map, i) { let mapptr: word = Typedef.rep(map) ; + let rawidx: word = Typedef.rep(i) ; + let loc: word = hash2(mapptr, rawidx) ; + return loc; } } +} +function rval(x: a) returns (b) where a: RValueMemberAccess { + return RValueMemberAccess.memberAccess(x); +} +function caller() returns (address) { + let res: word; + assembly { res := caller() + } + return address.address(res); +} +function require1fail() { + let res: word; + assembly { mstore(0, 2320231852978620534530211544385868) + revert(0, 32) + } + return (); +} +function require1(cond: Bool) { + match (cond) { case Bool.False { return require1fail(); } case Bool.True { return (); } } +} +function nop() returns () { + return (); +} +enum UintCxt { UintCxt } +enum reserved_sel { reserved_sel } +impl CStructField, reserved_sel>, word, ()> { +} +enum msg_sender_sel { msg_sender_sel } +impl CStructField, msg_sender_sel>, address, (word, ())> { +} +enum owner_sel { owner_sel } +impl CStructField, owner_sel>, address, (word, (address, ()))> { +} +enum decimals_sel { decimals_sel } +impl CStructField, decimals_sel>, uint, (word, (address, (address, ())))> { +} +enum totalSupply_sel { totalSupply_sel } +impl CStructField, totalSupply_sel>, uint, (word, (address, (address, (uint, ()))))> { +} +enum balances_sel { balances_sel } +impl CStructField, balances_sel>, mapping(address => uint), (word, (address, (address, (uint, (uint, ())))))> { +} +contract Uint { + function mint(amount: uint) public { + Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)))), Num.add(rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)))), amount)); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), totalSupply_sel.totalSupply_sel)), Num.add(rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), totalSupply_sel.totalSupply_sel)), amount)); + } + function transferFrom(src: address, dst: address, amt: uint) public returns (Bool) { + require1(ge(rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), src)), amt)); + withdraw(src, amt); + deposit(dst, amt); + return Bool.True; + } + function withdraw(src: address, amt: uint) public { + let balanceRef: storageRef = LValueMemberAccess.memberAccess(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), src)); + let updated: uint = Num.sub(rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), src)), amt); + Assign.assign(balanceRef, updated); + } + function deposit(dst: address, amt: uint) public { + let balanceRef: storageRef = LValueMemberAccess.memberAccess(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), dst)); + let updated: uint = Num.add(rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), dst)), amt); + Assign.assign(balanceRef, updated); + } + function init() public { + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)), address.address(81985529216486895)); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), msg_sender_sel.msg_sender_sel)), caller()); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), decimals_sel.decimals_sel)), Num.fromWord(18)); + } + function main() public returns (uint) { + init(); + mint(uint.uint(1000)); + mint(uint.uint(1000)); + let amt = uint.uint(1) ; + let src: address = rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)) ; + transferFrom(rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), msg_sender_sel.msg_sender_sel)), uint.uint(42)); + let checked: () = require1(Bool.True); + let balance: uint = rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), msg_sender_sel.msg_sender_sel)))); + return balance; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/noconstr.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/noconstr.solc new file mode 100644 index 00000000..4b9e2468 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/noconstr.solc @@ -0,0 +1,17 @@ +trait Foo { + function foo(x: a) returns (word) ; +} + +// here the constraint a : Foo is +// defered to outer scope where the +// error should be detected. + +function bla(x: a) returns (word) { + return Foo.foo(x); +} + +contract Test { + function main() public { + return bla(1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-detected.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-detected.solc new file mode 100644 index 00000000..32fc0bb0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-detected.solc @@ -0,0 +1,13 @@ +alias W = word; + +trait IdTy { + function id(x: self) returns (self) ; +} + +impl IdTy { + function id(x: W) returns (W) { return x; } +} + +impl IdTy { + function id(x: word) returns (word) { return 0; } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-order.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-order.solc new file mode 100644 index 00000000..1177b9cb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-order.solc @@ -0,0 +1,13 @@ +alias W = word; + +trait IdTy { + function id(x: self) returns (self) ; +} + +impl IdTy { + function id(x: word) returns (word) { return 0; } +} + +impl IdTy { + function id(x: W) returns (W) { return x; } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-two-synonyms.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-two-synonyms.solc new file mode 100644 index 00000000..37a1af58 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlap-synonym-missed-two-synonyms.solc @@ -0,0 +1,14 @@ +alias W = word; +alias V = word; + +trait IdTy { + function id(x: self) returns (self) ; +} + +impl IdTy { + function id(x: W) returns (W) { return x; } +} + +impl IdTy { + function id(x: V) returns (V) { return 0; } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlapping-heads.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlapping-heads.solc new file mode 100644 index 00000000..cf59fe4b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/overlapping-heads.solc @@ -0,0 +1,15 @@ +trait Foo { + function foo(x: a, y: word) returns (b) ; +} + +impl Foo<(), ()> { + function foo(x: (), y: word) returns () { + return (); + } +} + +impl Foo { + function foo(x: a, y: word) returns () { + return (); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/patterson-bug.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/patterson-bug.solc new file mode 100644 index 00000000..7dd914b3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/patterson-bug.solc @@ -0,0 +1,73 @@ +enum storage { storage(word) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +enum mapping { mapping(word, Proxy, Proxy) } // storage by default +// data mapRef(a) = mapRef(word); //ref to a map elem + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign, a> { + function assign(l: storageRef, y: a) returns () { + + } +} + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + + +impl LValueMemberAccess, storageRef> where StructField: CStructField { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + return storageRef.storageRef(0x100); + } +} + +// ------------------------------------------------------------------ +// Indexed access +// ------------------------------------------------------------------ + +enum mapping { mapping(word) } +enum IndexAccessProxy { IndexAccessProxy(map, index) } +enum IndexAccessProxy2 { IndexAccessProxy2(map, index, Proxy) } + +impl LValueMemberAccess member)>, index, member>, storageRef> { + function memberAccess(x: IndexAccessProxy, index, member>) returns (storageRef) { + return storageRef.storageRef(0); + } +} + +enum MintCtx { MintCtx } +enum balances_sel { balances_sel } +impl CStructField, mapping(word => word), ()> {} + + function mint(amount: word) { + let bal_prx = MemberAccessProxy.MemberAccessProxy(MintCtx.MintCtx, balances_sel.balances_sel); + let bal_ref = LValueMemberAccess.memberAccess(bal_prx); + + Assign.assign( + LValueMemberAccess.memberAccess( + IndexAccessProxy.IndexAccessProxy( + // bal_ref // this works, but inlining bal_ref leads to error + LValueMemberAccess.memberAccess(bal_prx) + , 0 + ) + ) + , amount + ) ; + + } diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.snap new file mode 100644 index 00000000..df2a4e2e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.snap @@ -0,0 +1,27 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.solc +--- +error[SC0001]: parse error: unexpected `payable` + --> /payable-toplevel-function.solc:3:1 + | +2 | // migrate-syntax: keep-legacy-negative +3 | payable function deposit() -> uint256 { + | ^^^^^^^ unexpected token +4 | return 0; + | + = note: expecting contract, interface, or library declaration, enum or struct declaration, export declaration, function declaration, impl declaration, import declaration, pragma declaration, trait declaration, or type declaration + = note: while parsing top-level item +--- + +error[SC0001]: parse error: unexpected `->` + --> /payable-toplevel-function.solc:3:28 + | +2 | // migrate-syntax: keep-legacy-negative +3 | payable function deposit() -> uint256 { + | ^^ unexpected token +4 | return 0; + | + = note: expecting `external`, `internal`, `payable`, `private`, `public`, `pure`, `returns`, `view`, `{`, or where clause + = note: while parsing function signature diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.solc new file mode 100644 index 00000000..3401894a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/payable-toplevel-function.solc @@ -0,0 +1,5 @@ +// The Classic prefix modifier order is no longer accepted. +// migrate-syntax: keep-legacy-negative +payable function deposit() -> uint256 { + return 0; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_coverage.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_coverage.solc new file mode 100644 index 00000000..5cd73114 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_coverage.solc @@ -0,0 +1,10 @@ +// Negative test for pragma merging - should fail +import * as pragma_merge_base from pragma_merge_base; + +trait TestFailClass {} + +enum FailType { FailType } + +// should fail because TestFailCoverage doesn't have no-coverage-condition +trait TestFailCoverage {} +impl TestFailCoverage, y> {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_patterson.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_patterson.solc new file mode 100644 index 00000000..56bd6526 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_fail_patterson.solc @@ -0,0 +1,11 @@ +// This file should FAIL compilation to demonstrate that checks are working when the imported file contains violations + +import * as pragma_merge_base from pragma_merge_base; + + +// --- Patterson Violation --- + +trait TestFailClass {} + +// Should fail because TestFailClass doesn't have no-patterson-condition +impl TestFailClass where U: TestClassP1, U: TestClassP2, U: TestClassP3 {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_import.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_import.solc new file mode 100644 index 00000000..17f1f7aa --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_import.solc @@ -0,0 +1,22 @@ +// Test import file for pragma merging functionality +// This file imports pragma_merge_base and adds its own violations with pragmas +// Testing that pragmas from both files are properly merged + +import * as pragma_merge_base from pragma_merge_base; + +// Add more pragmas - these should merge with imported ones + +trait TestClassC3 {} +trait TestClassB4 {} + +// fails coverage & patterson (pragma set here) +impl TestClassC3 where (i, j): TestClassP1 {} + +// fails coverage & patterson (pragma set in base) +impl TestClassP3 where (i, j): TestClassP1 {} + +// fails bound var & patterson (pragma set here) +impl TestClassB4> where c: TestClassB1 {} + +// fails bound var & patterson (pragma set in base) +impl TestClassB3> where c: TestClassB1 {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_verify.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_verify.solc new file mode 100644 index 00000000..f39dcab3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/pragma_merge_verify.solc @@ -0,0 +1,13 @@ +// Verification file for pragma merging +// This file imports pragma_merge_base but has no pragmas of its own +// Tests that pragmas from imported files are properly inherited + +import * as pragma_merge_base from pragma_merge_base; + +enum VerifyType { VerifyType } + +// Would fail without imported pragma no-patterson-condition TestClassP3 +impl TestClassP3 where (a, word): TestClassP3 {} + +// Would fail without imported pragma no-coverage-condition TestClassC1 +impl TestClassC1, q> {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/proxy1.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/proxy1.solc new file mode 100644 index 00000000..d221367e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/proxy1.solc @@ -0,0 +1,10 @@ +enum Proxy { Proxy } + +trait C { + function fun(p: Proxy) returns (word) ; +} + +function morefun(p: Proxy) returns (word) { + let proxy: Proxy = Proxy.Proxy; + return C.fun(proxy); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.snap new file mode 100644 index 00000000..66d3ba32 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.snap @@ -0,0 +1,14 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.solc +--- +error[SC0001]: constructor is implicitly public; remove the visibility keyword + --> /public-constructor.solc:5:19 + | +4 | contract PublicConstructor { +5 | constructor() public {} + | ^^^^^^ +6 | + | + = note: while parsing constructor definition diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.solc new file mode 100644 index 00000000..d9025298 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-constructor.solc @@ -0,0 +1,10 @@ +import std; +import std.dispatch; + +contract PublicConstructor { + constructor() public {} + + function answer() public returns (uint256) { + return uint256.uint256(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.snap new file mode 100644 index 00000000..d5fd7714 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.snap @@ -0,0 +1,25 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.solc +--- +error[SC0001]: `public` is not allowed on fallback; use `external` + --> /public-fallback.solc:7:16 + | +6 | +7 | fallback() public returns () { + | ^^^^^^ +8 | revert("fallback-was-called"); + | + = note: while parsing fallback definition +--- + +error[SC0001]: parse error: unexpected `)` + --> /public-fallback.solc:8:37 + | +7 | fallback() public returns () { +8 | revert("fallback-was-called"); + | ^ unexpected token +9 | } + | + = note: expecting `%=`, `&&`, `&=`, `&`, `(`, `*`, `+=`, `-=`, `.`, `;`, `<`, `=`, `>`, `?`, `[`, `^=`, `^`, `as`, `|=`, `|`, or `||` diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.solc new file mode 100644 index 00000000..ed5b94f3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-fallback.solc @@ -0,0 +1,10 @@ +import std; // migrate-syntax: keep-legacy-negative +import std.dispatch; + +contract PublicFallback { + constructor() {} + + fallback() public returns () { + revert("fallback-was-called"); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.snap new file mode 100644 index 00000000..380de8e7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.snap @@ -0,0 +1,27 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.solc +--- +error[SC0001]: parse error: unexpected `public` + --> /public-top-level-function.solc:6:1 + | +5 | // migrate-syntax: keep-legacy-negative +6 | public function answer() -> uint256 { + | ^^^^^^ unexpected token +7 | return uint256.uint256(42); + | + = note: expecting contract, interface, or library declaration, enum or struct declaration, export declaration, function declaration, impl declaration, import declaration, pragma declaration, trait declaration, or type declaration + = note: while parsing top-level item +--- + +error[SC0001]: parse error: unexpected `->` + --> /public-top-level-function.solc:6:26 + | +5 | // migrate-syntax: keep-legacy-negative +6 | public function answer() -> uint256 { + | ^^ unexpected token +7 | return uint256.uint256(42); + | + = note: expecting `external`, `internal`, `payable`, `private`, `public`, `pure`, `returns`, `view`, `{`, or where clause + = note: while parsing function signature diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.solc new file mode 100644 index 00000000..1ec5b8d9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/public-top-level-function.solc @@ -0,0 +1,8 @@ +import std; +import std.dispatch; + +// The Classic prefix modifier order is no longer accepted. +// migrate-syntax: keep-legacy-negative +public function answer() -> uint256 { + return uint256.uint256(42); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-encoding.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-encoding.solc new file mode 100644 index 00000000..840b629a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-encoding.solc @@ -0,0 +1,223 @@ + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + return Typedef.abs(MemoryType.load(ptr)); + } + function store(ptr: word, value: uint) returns () { + return MemoryType.store(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +// This is *a lot* of pragmas... +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(Typedef.abs(ptr)); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let bProxy: Proxy = Proxy.Proxy; + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + // BUG: Something wrong here? Complains about ptr not being word... + /*assembly { + ptr := add(ptr, size) + }*/ + let loaded: fieldType = MemoryType.load(Typedef.abs(ptr)); + return loaded; + } +} + +////// Testing + +// struct S { x:word; y:uint; z:word; } +enum S { S(word, uint, word) } +enum x_sel { x_sel } +enum y_sel { y_sel } +enum z_sel { z_sel } + +impl CStructField, word, ()> {} +impl CStructField, uint, word> {} +// BUG: This next one should really be the following, but that breaks weirdly: +// (I get a patterson condition violation on an invoke instance for g) +// instance StructField(S, z_sel):CStructField(word, (word,uint)) {} +// So instead I use: +impl CStructField, word, word> {} + + +function f() { + let x: word memory; + let y: word memory; + // x = y + Assign.assign(ref.ref(x), y); + /* + * Idea in the above: to avoid overlapping instances, + * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), + * to be able to choose a disjoint assign instance. + * Of course this needs special treatment during code generation, + * on the other hand, stack assignments generally do... + * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. + */ +} + +function g() { + let s: S memory = Typedef.abs(0x80); + let y: word = 42; + let z: uint = uint.uint(42); + // s.x = y + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel)), y); + // s.y = 21 + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, y_sel.y_sel)), z); + // s.z = y; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), y); + // y = s.x + Assign.assign(ref.ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); + // s.z = s.x + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); +} +contract C { + function main() public { + f(); + g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-test.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-test.solc new file mode 100644 index 00000000..0022f431 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference-test.solc @@ -0,0 +1,49 @@ +enum memory { memory(word) } + +trait Typedef { + function abs(v: rep) returns (abs) ; + function rep(v: abs) returns (rep) ; +} + +impl Typedef { + function abs(ptr: word) returns (a memory) { + return memory.memory(ptr); + } + function rep(v: a memory) returns (word) { + match (v) { case memory.memory(ptr) { return ptr; } } + } +} + +trait Test { + function test(x: self) returns (word) ; +} + +impl Test { + function test(x: word) returns (word) { + return x; + } +} + +enum test { test(a memory) } + +impl Typedef, a memory> { + function rep(x: test) returns (a memory) { + match (x) { case test.test(m) { return m; } } + } + function abs(m: a memory) returns (test) { + return test.test(m); + } +} + +impl Test> where test: Typedef, rep: Test { + function test(x: test) returns (word) { + return Test.test(Typedef.rep(x)); + } + } + +contract C { + function main() public { + let x: test = test.test(memory.memory(42)); + let ptr: word = Test.test(x); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference.solc new file mode 100644 index 00000000..a73375e3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/reference.solc @@ -0,0 +1,31 @@ +trait Ref { + function load(r: ref) returns (deref) ; + function store(r: ref, v: deref) returns (unit) ; +} + +enum stack { stack(a) } + +impl Ref, a> { +} + +enum MemberAccess { MemberAccess(ty) } + +enum PairFst { PairFst } +enum PairSnd { PairSnd } + +enum XRef { XRef(st, field) } +impl Ref, a> where r: Ref {} +impl Ref, b> where r: Ref {} + +contract AssignNested { + function main() public { + let x: stack<(word, (word, word))>; + let z: stack<(word, (word, word))>; + + // either of the next lines is fine on their own, but not together + Ref.store( XRef.XRef(z,PairFst.PairFst), 21); + Ref.store( XRef.XRef(XRef.XRef(x, PairSnd.PairSnd), PairFst.PairFst), 20 ); + + return 77; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/references-daniel.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/references-daniel.solc new file mode 100644 index 00000000..578ddc26 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/references-daniel.solc @@ -0,0 +1,228 @@ +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + +enum xunit { xunit } + +enum uint { uint(word) } + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + return Typedef.abs(MemoryType.load(ptr)); + } + function store(ptr: word, value: uint) returns () { + return MemoryType.store(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, Proxy) } + +function memberAccessPtr(x: MemberAccessProxy) returns (word) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { match (y) { case memory.memory(ptr) { return ptr; } } } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +enum zero { zero } +enum suc { suc(a) } + +impl Typedef, word> {} +impl LValueMemberAccess, memoryRef> { + function memberAccess(mptr: MemberAccessProxy<(a, b) memory, zero>, f: Proxy) returns (memoryRef) { + let ptr: word = Typedef.rep(mptr); + return memoryRef.memoryRef(ptr); + } +} + +impl LValueMemberAccess>, c> where MemberAccessProxy: LValueMemberAccess, a: MemorySize { + function memberAccess(map: MemberAccessProxy<(a, b) memory, suc>, f: Proxy>) returns (c) { + let ptr: word = memberAccessPtr(map); + let aProxy: Proxy = Proxy.Proxy; + let sz: word = MemorySize.size(aProxy); + assembly { ptr := add(ptr, sz) } + let newPtr: b memory = memory.memory(ptr); + let fieldProxy: Proxy = Proxy.Proxy; + return LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(newPtr, fieldProxy)); + } +} + +impl LValueMemberAccess, word> {} +impl LValueMemberAccess>, uint> {} +impl LValueMemberAccess>>, word> {} +impl Assign {} +impl Assign {} + +////// Testing + +// struct S { x:word; y:uint; z:word; } +enum S { S(word, uint, word) } +enum x_sel { x_sel } +enum y_sel { y_sel } +enum z_sel { z_sel } + +impl Typedef { + function abs(x: (word, uint, word)) returns (S) { + match (x) { case (a, b, c) { return S.S(a, b, c); } } + } + function rep(x: S) returns (word, uint, word) { + match (x) { case S.S(a, b, c) { return (a, b, c); } } + } +} + + +// The idea here would be to generate these particularly on the definition of a struct with fields. +impl LValueMemberAccess, word> where S: Typedef, MemberAccessProxy: LValueMemberAccess { + function memberAccess(map: MemberAccessProxy, f: Proxy) returns (word) { + let memoryValue: rep memory = memory.memory(memberAccessPtr(map)); + let fieldProxy: Proxy = Proxy.Proxy; + let value: word = LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(memoryValue, fieldProxy)); + return value; + } +} + +impl LValueMemberAccess, uint> where S: Typedef, MemberAccessProxy>: LValueMemberAccess { + function memberAccess(map: MemberAccessProxy, f: Proxy) returns (uint) { + let memoryValue: rep memory = memory.memory(memberAccessPtr(map)); + let fieldProxy: Proxy> = Proxy.Proxy; + return LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(memoryValue, fieldProxy)); + } +} + +impl LValueMemberAccess, word> where S: Typedef, MemberAccessProxy>>: LValueMemberAccess { + function memberAccess(map: MemberAccessProxy, f: Proxy) returns (word) { + let memoryValue: rep memory = memory.memory(memberAccessPtr(map)); + let fieldProxy: Proxy>> = Proxy.Proxy; + return LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(memoryValue, fieldProxy)); + } +} + +function f() { + let x: word memory; + let y: word memory; + x = y; +} + +function g() { + let s: S memory = Typedef.abs(0x80); + let x: word = 42; + let y: uint = Typedef.abs(21); + let z: word = 7; + // s.x = x; + let xProxy: Proxy = Proxy.Proxy; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, xProxy)), x); + // s.y = y; + let yProxy: Proxy = Proxy.Proxy; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, yProxy)), y); + // s.z = z; + let zProxy: Proxy = Proxy.Proxy; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, zProxy)), z); +} + +contract C { + function main() public { + f(); + g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-contract-method.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-contract-method.solc new file mode 100644 index 00000000..fe6e0760 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-contract-method.solc @@ -0,0 +1,10 @@ +// Error: contract method missing return type annotation +contract Doubler { + function double(x: word) public { + return x; + } + + function main() public returns (word) { + return double(21); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-both.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-both.solc new file mode 100644 index 00000000..da363359 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-both.solc @@ -0,0 +1,4 @@ +// Error: top-level free function with no annotations at all +function id(x) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-param.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-param.solc new file mode 100644 index 00000000..03099aeb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-param.solc @@ -0,0 +1,6 @@ +// Error: top-level free function with an unannotated parameter +function add(x, y: word) returns (word) { + let res: word; + assembly { res := add(x, y) } + return res; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-return.solc new file mode 100644 index 00000000..5ebbbc6b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-missing-return.solc @@ -0,0 +1,6 @@ +// Error: top-level free function with no explicit return type +function double(x: word) { + let res: word; + assembly { res := add(x, x) } + return res; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-mutual.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-mutual.solc new file mode 100644 index 00000000..7b4081e1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/require-annotation-mutual.solc @@ -0,0 +1,8 @@ +// Error: mutually recursive free functions without annotations +function foo(x: word) { + return bar(x); +} + +function bar(x: word) returns (word) { + return foo(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-arity.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-arity.solc new file mode 100644 index 00000000..51df0e68 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-arity.solc @@ -0,0 +1,11 @@ +// INCORRECT: the signature promises a one-argument function (word) -> word, +// but the returned lambda takes two arguments. +function makeF(x: word) returns (function(word) returns (word)) { + return lam (y: word, z: word) returns (word) { + let res: word; + assembly { + res := add(y, z) + } + return res; + }; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-param.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-param.solc new file mode 100644 index 00000000..fa28f5f1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-param.solc @@ -0,0 +1,8 @@ +// INCORRECT: the returned lambda's parameter is `bool`, but the signature +// promises (word) -> word. Closure conversion would erase the arrow type; +// the single-pass checker must still reject this. +function makeAdder(x: word) returns (function(word) returns (word)) { + return lam (y: bool) returns (word) { + return x; + }; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-return.solc new file mode 100644 index 00000000..73cb8832 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-return.solc @@ -0,0 +1,7 @@ +// INCORRECT: the returned lambda's body has type bool, but the signature +// promises the result is word. +function makeConst(x: word) returns (function(word) returns (word)) { + return lam (y: word) returns (bool) { + return true; + }; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-sig.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-sig.solc new file mode 100644 index 00000000..392c91cb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-bad-sig.solc @@ -0,0 +1,7 @@ +// INCORRECT: signature says the result consumes a bool ((bool) -> word), +// but the returned lambda consumes a word. +function makeF(x: word) returns (function(bool) returns (word)) { + return lam (y: word) returns (word) { + return x; + }; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-not-fun.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-not-fun.solc new file mode 100644 index 00000000..8f407cbb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/return-fun-not-fun.solc @@ -0,0 +1,5 @@ +// INCORRECT: the signature promises a function (word) -> word, but the body +// returns a plain word instead of a function. +function makeF(x: word) returns (function(word) returns (word)) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/signature.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/signature.solc new file mode 100644 index 00000000..fa0229c4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/signature.solc @@ -0,0 +1,8 @@ +trait Typedef { + function rep(x: self) returns (underlyingType) ; +} + + +function tripleFun(x: t) where t: Typedef { + return Typedef.rep(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfExpr.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfExpr.solc new file mode 100644 index 00000000..230c75a5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfExpr.solc @@ -0,0 +1,3 @@ +contract SimpleIfStmt { + function main() public { return (((true) ? 1 : 0)); } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfStmt.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfStmt.solc new file mode 100644 index 00000000..c81311f0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/simpleIfStmt.solc @@ -0,0 +1,3 @@ +contract SimpleIfStmt { + function main() public { if (true) {return 1;} else {return 0;} } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/skolem-let.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/skolem-let.solc new file mode 100644 index 00000000..05362c46 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/skolem-let.solc @@ -0,0 +1,13 @@ + +function fromWord(x: word) returns (a) { + let result: a; + assembly { result := x } + return result; + } + +contract Unsafe { + function main() public { + let result: () = fromWord(7); + return 42; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/string-const.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/string-const.solc new file mode 100644 index 00000000..58e78346 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/string-const.solc @@ -0,0 +1,5 @@ +contract Answer { + function main() public { + return "42"; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-index.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-index.solc new file mode 100644 index 00000000..3c1a7290 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-index.solc @@ -0,0 +1,74 @@ +enum storage { storage(word) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +enum mapping { mapping(word, Proxy, Proxy) } + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign, a> { + function assign(l: storageRef, y: a) { + } +} + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + +impl LValueMemberAccess, storageRef> where StructField: CStructField { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + return storageRef.storageRef(0x100); + } +} + +// ------------------------------------------------------------------ +// Indexed access +// ------------------------------------------------------------------ + +enum mapping { mapping(word) } +enum IndexAccessProxy { IndexAccessProxy(map, index) } + +impl LValueMemberAccess, index, member>, storageRef> { + function memberAccess(x: IndexAccessProxy, index, member>) returns (storageRef) { + return storageRef.storageRef(0); + } +} + +enum MintCtx { MintCtx } +enum balances_sel { balances_sel } +impl CStructField, mapping(word => word), ()> {} + + function mint(amount: word) { + let bal_prx = MemberAccessProxy.MemberAccessProxy(MintCtx.MintCtx, balances_sel.balances_sel); + let bal_ref = LValueMemberAccess.memberAccess(bal_prx); + + Assign.assign( + LValueMemberAccess.memberAccess( + IndexAccessProxy.IndexAccessProxy( + // bal_ref // this works, but inlining bal_ref leads to error + LValueMemberAccess.memberAccess(bal_prx) + , 0 + ) + ) + , amount + ) ; + + } +contract Map { + function main() public { + mint(1000); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-reduction.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-reduction.solc new file mode 100644 index 00000000..62389a73 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subject-reduction.solc @@ -0,0 +1,77 @@ +enum storage { storage(word) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +enum mapping { mapping(word, Proxy, Proxy) } + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign, a> { + function assign(l: storageRef, y: a) { + } +} + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + +impl LValueMemberAccess, storageRef> where StructField: CStructField { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + return storageRef.storageRef(0x100); + } +} + +// ------------------------------------------------------------------ +// Indexed access +// ------------------------------------------------------------------ + +enum mapping { mapping(word) } +enum IndexAccessProxy { IndexAccessProxy(map, index) } + +impl LValueMemberAccess, index, member>, storageRef> { + function memberAccess(x: IndexAccessProxy, index, member>) returns (storageRef) { + return storageRef.storageRef(0); + } +} + + +enum MintCtx { MintCtx } +enum balances_sel { balances_sel } +impl CStructField, mapping(word => word), ()> {} + + function mint(amount: word) { + let bal_prx = MemberAccessProxy.MemberAccessProxy(MintCtx.MintCtx, balances_sel.balances_sel); + let bal_ref = LValueMemberAccess.memberAccess(bal_prx); + + Assign.assign( + LValueMemberAccess.memberAccess( + IndexAccessProxy.IndexAccessProxy( + // bal_ref // this works, but inlining bal_ref leads to error + LValueMemberAccess.memberAccess(bal_prx) + , 0 + ) + ) + , amount + ) ; + + } +/* +contract Map { + function main () { + mint(1000); + } +} +*/ diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-constraint.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-constraint.solc new file mode 100644 index 00000000..42135de5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-constraint.solc @@ -0,0 +1,22 @@ +// This code should FAIL, but PASSES! +enum Bool { True, False } + +trait MyCls { + function f(x: a, y: a) returns (Bool) ; +} + +function the_bug(x: a, y: a) returns (Bool) { + return MyCls.f(x, y); +} + +contract Foo { + function x() public { + let b1 = Bool.True; + let b2 = Bool.False; + the_bug(b1, b2); + } + + function main() public { + x(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-test.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-test.solc new file mode 100644 index 00000000..1659e1cf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/subsumption-test.solc @@ -0,0 +1,7 @@ +function id(x) returns (word) { + return x; +} + +function fakeid(x: word) returns (a) { + return x ; +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-cycle-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-cycle-fail.solc new file mode 100644 index 00000000..630bdd2d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-cycle-fail.solc @@ -0,0 +1,15 @@ +trait A where a: B {} +trait B where a: A {} +trait C {} + +function needsC(x: a) returns () where a: C { + return (); +} + +function cannotGetC(x: a) returns () where a: A { + return needsC(x); +} + +function main() returns () { + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-recursive-arg.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-recursive-arg.solc new file mode 100644 index 00000000..7256bdc5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/super-class-recursive-arg.solc @@ -0,0 +1,17 @@ +pragma solcore noPattersonCondition A; + +enum Wrap { Wrap(a) } + +trait A where Wrap: A {} + +function needsWrappedA(x: a) returns () where Wrap: A { + return (); +} + +function shouldUseSuperclass(x: a) returns () where a: A { + return needsWrappedA(x); +} + +function main() returns () { + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-arity-mismatch.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-arity-mismatch.solc new file mode 100644 index 00000000..39c42e92 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-arity-mismatch.solc @@ -0,0 +1,5 @@ +alias F = pair; + +function main() returns (F) { + return pair(42, 0); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-long-cycle.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-long-cycle.solc new file mode 100644 index 00000000..f44442a7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-long-cycle.solc @@ -0,0 +1,8 @@ +// Longer recursive cycle should be rejected +alias A = B; +alias B = C; +alias C = A; + +function main() returns (word) { + return 0; +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-recursive.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-recursive.solc new file mode 100644 index 00000000..272e8898 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-recursive.solc @@ -0,0 +1,8 @@ +alias A = B; +alias B = A; + +contract RecursiveTest { + function main() public returns (word) { + return 0; + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-self-recursive.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-self-recursive.solc new file mode 100644 index 00000000..be98a241 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/synonym-self-recursive.solc @@ -0,0 +1,6 @@ +// Self-recursive synonym should be rejected +alias A = A; + +function main() returns (word) { + return 0; +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-answer-reuse.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-answer-reuse.solc new file mode 100644 index 00000000..7a118515 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-answer-reuse.solc @@ -0,0 +1,16 @@ +pragma solcore noPattersonCondition Derived; + +trait Seed {} +trait Derived {} + +impl Seed {} + +impl Derived where a: Seed {} + +function needsDerivedTwice(x: a) returns () where a: Derived, a: Derived { + return (); +} + +function main() returns () { + return needsDerivedTwice(0); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-cycle-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-cycle-fail.solc new file mode 100644 index 00000000..55906337 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-cycle-fail.solc @@ -0,0 +1,16 @@ +pragma solcore noPattersonCondition A; +pragma solcore noPattersonCondition B; + +trait A {} +trait B {} + +impl A where a: B {} +impl B where a: A {} + +function needsA(x: a) returns () where a: A { + return (); +} + +function main() returns () { + return needsA(0); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-left-recursive-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-left-recursive-fail.solc new file mode 100644 index 00000000..a0ef0579 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-left-recursive-fail.solc @@ -0,0 +1,13 @@ +pragma solcore noPattersonCondition Loop; + +trait Loop {} + +impl Loop where a: Loop {} + +function needsLoop(x: a) returns () where a: Loop { + return (); +} + +function main() returns () { + return needsLoop(0); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-mutual-chain.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-mutual-chain.solc new file mode 100644 index 00000000..c3e9ef65 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/tabled-mutual-chain.solc @@ -0,0 +1,18 @@ +enum WrapA { WrapA(a) } +enum WrapB { WrapB(a) } + +trait A {} +trait B {} + +impl A {} + +impl B> where a: A {} +impl A> where a: B {} + +function needsA(x: a) returns () where a: A { + return (); +} + +function main() returns () { + return needsA(WrapA.WrapA(WrapB.WrapB(0))); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.snap new file mode 100644 index 00000000..4529b2d3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.snap @@ -0,0 +1,12 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.solc +--- +error[SC0001]: could not parse top-level item near `constructor() {}`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /toplevel-constructor.solc:3:1 + | +1 | // A `constructor` may only be declared inside a contract. +2 | // At the top level this must fail to parse. +3 | constructor() {} + | ^^^^^^^^^^^^^^^^ diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.solc new file mode 100644 index 00000000..2bd579da --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-constructor.solc @@ -0,0 +1,3 @@ +// A `constructor` may only be declared inside a contract. +// At the top level this must fail to parse. +constructor() {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.snap new file mode 100644 index 00000000..f66409a2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.snap @@ -0,0 +1,12 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.solc +--- +error[SC0001]: could not parse top-level item near `fallback() returns () {}`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /toplevel-fallback.solc:3:1 + | +1 | // A `fallback` may only be declared inside a contract. +2 | // At the top level this must fail to parse. +3 | fallback() returns () {} + | ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.solc new file mode 100644 index 00000000..29e5f061 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/toplevel-fallback.solc @@ -0,0 +1,3 @@ +// A `fallback` may only be declared inside a contract. +// At the top level this must fail to parse. +fallback() returns () {} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unbound-instance-var.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unbound-instance-var.solc new file mode 100644 index 00000000..86a8b1bc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unbound-instance-var.solc @@ -0,0 +1,15 @@ +trait C { + function size(x: self) returns (word) ; +} + +impl C<()> { + function size(x: ()) returns (word) { + return 0; + } +} + +impl C { + function size(x: uint) returns (word) { + return 1; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unconstrained-instance.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unconstrained-instance.solc new file mode 100644 index 00000000..dea5b246 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/unconstrained-instance.solc @@ -0,0 +1,22 @@ +enum memory { memory(word) } + +trait ValueTy { + function rep(x: t) returns (word) ; +} + +impl ValueTy { + function rep(x: t memory) returns (word) { + match (x) { case memory.memory(w) { return w; } } + } +} + +trait Ref { + function store(loc: ref, value: deref) returns () ; +} + +impl Ref { + function store(loc: t memory, value: t) returns () { + // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... + let vw = ValueTy.rep(value); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.snap b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.snap new file mode 100644 index 00000000..e52449ce --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.snap @@ -0,0 +1,24 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.solc +--- +error[SC0001]: could not parse top-level item near `infixl 70 (^^) => pow;`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /user-op-lambda.solc:6:1 + | +5 | +6 | infixl 70 (^^) => pow; + | ^^^^^^^^^^^^^^^^^^^^^^ +7 | + | +--- + +error[SC0001]: parse error: unexpected `^` + --> /user-op-lambda.solc:17:54 + | +16 | // operator (^^) used inside a lambda body +17 | let f = lam (x: word) returns (word) { return x ^^ 3; }; + | ^ unexpected token +18 | return f(2); + | + = note: expecting `!`, `(`, `@`, or `lam` diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.solc new file mode 100644 index 00000000..7320ecc6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/user-op-lambda.solc @@ -0,0 +1,20 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +infixl 70 (^^) => pow; + +function pow(b: word, e: word) returns (word) { + let r: word; + assembly { r := exp(b, e) } + return r; +} + +contract UserOpLambda { + function main() returns (word) { + // operator (^^) used inside a lambda body + let f = lam (x: word) returns (word) { return x ^^ 3; }; + return f(2); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/vartyped.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/vartyped.solc new file mode 100644 index 00000000..317e9db8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/vartyped.solc @@ -0,0 +1,4 @@ +function foo() { + let f: function(word) returns (word) = lam (x) { return x; }; + return f(1); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weird-error-foo.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weird-error-foo.solc new file mode 100644 index 00000000..a1c86ddb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weird-error-foo.solc @@ -0,0 +1 @@ +function foo(x: word) { return foo(word); } diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weirdfoo.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weirdfoo.solc new file mode 100644 index 00000000..69952ad5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/weirdfoo.solc @@ -0,0 +1,5 @@ +enum W { W(a) } +trait Foo {function foo(); } +impl Foo<(word, W)> where (word, a): Foo { + function foo() {} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/xref.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/xref.solc new file mode 100644 index 00000000..6c70562f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/xref.solc @@ -0,0 +1,139 @@ +function add_(x: word, y: word) { // _add is not a legal identifier :( + let res: word; + assembly { + res := add(x, y) + } + return res; + } + +function mload_(x: word) returns (word) { + let res: word; + assembly { + res := mload(x) + } + return res; + } + +function mstore_(a: word, v: word) { + assembly { mstore(a,v) } +} + +trait Ref { function load(x: r) returns (d) ; function store(x: r, v: d) returns () ;} + +trait Typedef { + function rep(x: self) returns (underlyingType) ; // abbr: x.rep = Typedef.rep(x) + function abs(x: underlyingType) returns (self) ; // abbr: x.abs +} +enum Proxy { Proxy } + +enum M { M(word) } + +impl Typedef, word> { + function rep(m: M) returns (word) { match (m) { case M.M(w) { return w; } }} + function abs(w: word) returns (M) { return M.M(w); } +} + +trait MemoryType { + function memorySize(p: Proxy) returns (word) ; + /* inline function sizeof(Self) -> word { // an abbreviation to avoid writing Proxy; wasteful unless inlined + return memorySize(Proxy:Proxy(self)); + } */ + function memoryStep(word, self: Self) returns (word) ; + function mload(r: word) returns (Self) ; + function mstore(r: word, v: Self) returns () ; +} + +function sizeof(self: Self) returns (word) where Self: MemoryType { + let proxy: Proxy = Proxy.Proxy; + return MemoryType.memorySize(proxy); +} + +trait MemoryRef { function addr(r: a) returns (word) ; } +impl MemoryRef, a> { function addr(r: M) returns (word) {return Typedef.rep(r);} } + +function xaddr(r: M) returns (word) { return MemoryRef.addr(r); } +function asMemRefTo(r: M, p: Proxy) returns (M) { return Typedef.abs(xaddr(r)); } + +function stepStore(aa: word, va: a) returns (word) where a: MemoryType { + MemoryType.mstore(aa, va); + let proxy: Proxy = Proxy.Proxy; + return add_(aa, MemoryType.memorySize(proxy)); +} + +impl Ref where Self: MemoryType, r: MemoryRef { + function load(r: M) returns (Self) { return MemoryType.mload(xaddr(r)); } + function store(r: M, v: Self) returns () { MemoryType.mstore(xaddr(r), v); } +} + +impl MemoryType { + function memorySize(p: Proxy) returns (word) { return 32; } + function memoryStep(a: word, self: word) returns (word) { return add_(a,32); } + function mload(a: word) returns (word) { return mload_(a); } + function mstore(a: word, v: word) returns () { mstore_(a, v); } +} + +impl MemoryType<(a, b)> where a: MemoryType, b: MemoryType { + function memorySize(p: Proxy<(a, b)>) returns (word) { + let leftProxy: Proxy = Proxy.Proxy; + let leftSize: word = MemoryType.memorySize(leftProxy); + let rightProxy: Proxy = Proxy.Proxy; + let rightSize: word = MemoryType.memorySize(rightProxy); + return add_(leftSize, rightSize); + } + + function mload(aa: word) returns (a, b) { + let va = MemoryType.mload(aa); + let ab = add_(aa, sizeof(va)); + let vb = MemoryType.mload(ab); + return (va,vb); + } + + function mstore(aa: word, v: (a, b)) returns () { + match (v) { case pair(va, vb) { mstore2(aa, va, vb); } } // match-compiler cannot compile mopre than 1 stmt in a branch :( + } +} + +function mstore2(aa: word, va: a, vb: b) where a: MemoryType, b: MemoryType { //needed because of bug in match-compiler + let ab = stepStore(aa, va); + MemoryType.mstore(ab, vb); +} + +enum XRef { XRef(st, field) } +enum PairFst { PairFst } +enum PairSnd { PairSnd } + + +impl MemoryRef, a> where r: MemoryRef<(a, b)>, a: MemoryType, b: MemoryType { + function addr(xr: XRef) returns (word) { + match (xr) { case XRef.XRef(r, _) { return MemoryRef.addr(r); } } + } +} + +impl MemoryRef, b> where r: MemoryRef<(a, b)>, a: MemoryType, b: MemoryType { + function addr(xr: XRef) returns (word) { + match (xr) { + case XRef.XRef(r, _) { + let proxy: Proxy = Proxy.Proxy; + return add_(MemoryRef.addr(r), MemoryType.memorySize(proxy)); + } + } + } +} + +contract Ref219 { + function main() public { + let mp: M<(word, word, word)> = M.M(96); // no alloc yet + let p = (1,16,25); + Ref.store(mp, p); + + let ra = XRef.XRef(mp, PairFst.PairFst); + let a = Ref.load(ra); + let r2 = XRef.XRef(mp, PairSnd.PairSnd); + let rb = XRef.XRef(r2, PairFst.PairFst); + let a = Ref.load(ra); + let b = Ref.load(rb); + let rc = XRef.XRef(r2, PairSnd.PairSnd); + let c = Ref.load(rc); + return add_(a, add_(b, c)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/yul-multi-return-arity-fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/yul-multi-return-arity-fail.solc new file mode 100644 index 00000000..7b19d0e7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/cases/yul-multi-return-arity-fail.solc @@ -0,0 +1,18 @@ +// The arity check must still reject a genuine mismatch: 'pair' returns 2 +// values but 3 names are being assigned, so this Yul is invalid and the type +// checker must report the arity error. +contract YulMultiRetBad { + function main() public returns (word) { + let x: word; + let y: word; + let z: word; + assembly { + function pair() -> a, b { + a := 1 + b := 2 + } + x, y, z := pair() + } + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/OneOne.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/OneOne.solc new file mode 100644 index 00000000..7dfa6a6e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/OneOne.solc @@ -0,0 +1,14 @@ +function addWord(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l,r); + } + return rw; +} + +function zero() { return 0; } +function one() { return addWord(1, zero()); } + +contract OneOne { + function main() returns (word) { return addWord(one(), one()); } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_poly_runtime.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_poly_runtime.solc new file mode 100644 index 00000000..207c969f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_poly_runtime.solc @@ -0,0 +1,27 @@ +/* Negative: comptime violation in a polymorphic (generic) function. + Before specialisation the concrete type of 'z' is unknown, so this + cannot be resolved by inlining. The SAIL-level check catches the + violation: 'z' is a non-comptime parameter and cannot satisfy the + comptime contract of 'unwrap'. +*/ +import * as std from std; + +trait Wrap { + function unwrap(comptime x: t) returns (comptime word) ; +} + +impl Wrap { + function unwrap(comptime x: word) returns (comptime word) { + return x; + } +} + +function process(z: t) returns (word) where t: Wrap { + return Wrap.unwrap(z); +} + +contract ComptimeParamPolyRuntime { + function main() returns (word) { + return process(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_runtime.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_runtime.solc new file mode 100644 index 00000000..86887d98 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/ct_param_runtime.solc @@ -0,0 +1,19 @@ +/* Negative: non-comptime function parameter passed to a comptime parameter. + Caught by the SAIL-level check: 'process' CAN be called with an argument + not known at compile time, which would violate the comptime requirement + of 'double'. The SAIL check rejects this on the parameter type alone, + before looking at specific call sites. +*/ +import * as std from std; + +contract ComptimeParamRuntime { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function process(value: word) returns (word) { + return double(value); + } + function main() returns (word) { + return process(21); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt.solc new file mode 100644 index 00000000..1847c9e1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt.solc @@ -0,0 +1,92 @@ +/* Handling numeric literals + +Eventually we may want to have a comptime integer type (unlimited precision) +and literals desugar to `fromInteger(lit)` + +Here we use a bit less ambitious approach: literals of type word and `fromWord` method +*/ + +import * as std from std; + +alias uint = uint256; // misleads instance solver + +trait Int { + function fromWord(x: word) returns (comptime i) ; // meaning result is comptime whenever arg is + + function toWord(x: i) returns (comptime word) ; +} + + +impl Int { + function fromWord(x: word) returns (comptime word) { return x; } + function toWord(x: word) returns (comptime word) { return x; } +} + +impl Int { + function fromWord(x: word) returns (comptime uint) { return uint256.uint256(x); } + function toWord(x: uint) returns (comptime word) { return Typedef.rep(x); } +} + + +// specialised for numbers +function fromInt(x: a) returns (b) where a: Int, b: Int { return Int.fromWord(Int.toWord(x)); } +function staticInt(comptime x: a) returns (comptime b) where a: Int, b: Int { return Int.fromWord(Int.toWord(x)); } + +// limited usability +function dynamic_cast(x: a) returns (b) where a: Typedef, b: Typedef { + let rep: r = Typedef.rep(x); + return Typedef.abs(rep); +} +function static_cast(comptime x: a) returns (comptime b) where a: Typedef, b: Typedef { + let rep: r = Typedef.rep(x); + return Typedef.abs(rep); +} + +// wider usability +function dynamic_cast_via(p: @r, x: a) returns (b) where a: Typedef, b: Typedef { + let rep: r = Typedef.rep(x); + return Typedef.abs(rep); +} + +function static_cast_via(comptime p: @r, comptime x: a) returns (comptime b) where a: Typedef, b: Typedef { + let rep: r = Typedef.rep(x); + return Typedef.abs(rep); +} +// maybe: `comptime function static_cast_via` as equivalent notation + +function notcomptime(x: word) returns (word) { + let res: word; + assembly { + res := mload(0) + } + return res; +} + +function id(x: a) returns (comptime a) { return x; } +function id_uint(x: uint) returns (comptime uint) { return x; } +contract FromWord { + constructor() {} + function f1(x: word) returns (comptime word) { return x; } + function f2(x: uint) returns (comptime uint) { return x; } + function g() returns (uint) { + let comptime y1: uint256 = static_cast( // cast on top level of comptime let + f1( + static_cast(42) //cast a literal - could be fromWord/staticInt + )); + + let comptime y2: uint256 = staticInt( id_uint(staticInt(42)) ); // cast at literal, cast at let + + let z = notcomptime(Typedef.rep(y1)); // no cast - not comptime + let t = dynamic_cast(y1); // just testing + return t; + } + + function h() returns (comptime uint256) { + let comptime inner: uint256 = staticInt(42); + let comptime y2: uint256 = staticInt(inner); // error without the type annotation + return y2; + } + function main() { + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt2.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt2.solc new file mode 100644 index 00000000..2a916015 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt2.solc @@ -0,0 +1,46 @@ +// import std.{Num,Add,Sub,Eq,Ord,Bounded,Typedef,le}; +import * as std from std; + +trait Int { + function fromWord(x: word) returns (comptime i) ; // meaning result is comptime whenever arg is + + function toWord(x: i) returns (comptime word) ; +} + +impl Int { + function fromWord(x: word) returns (uint256) { return Typedef.abs(x); } + function toWord(y: uint256) returns (word) { return Typedef.rep(y); } +} + +impl Mul { + function mul(x: uint256, y: uint256) returns (uint256) { + return Int.fromWord(Mul.mul(Int.toWord(x), Int.toWord(y))); + } +} +impl Int { + function fromWord(x: word) returns (word) { return x; } + function toWord(y: word) returns (word) { return y; } +} + +function bitAnd(x: word, y: word) returns (comptime word) { + let res: word; + assembly { + res := and(x,y) + } + return res; +} +function fromLit(x: word) returns (a) where a: Num { return Num.fromWord(x); } + +contract FromInt { + function main() returns (uint256) { + let a: uint256 = fromLit(1); + let comptime b: uint256 = fromLit((2 + 2)); // CTE + let c: uint256 = fromLit(3) + fromLit(3); // RTE + // let d : comptime word = fromLit(bitAnd(0xff,keccakLit("foo"+"bar"))); // CTE + let comptime d: word = fromLit(bitAnd(0xff,keccakLit("foo"+"bar"))); // CTE + + let k = fromLit(40); + return k+2; + // return b*b + fromLit(4)*a*c + fromLit(d); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt3.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt3.solc new file mode 100644 index 00000000..54f2a0b9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromInt3.solc @@ -0,0 +1,39 @@ +// import std.{Num,Add,Sub,Eq,Ord,Bounded,Typedef,le}; +import * as std from std; + +trait Int { + function fromWord(x: word) returns (comptime i) ; // meaning result is comptime whenever arg is + + function toWord(x: i) returns (comptime word) ; +} + +impl Int { + function fromWord(x: word) returns (uint256) { return Typedef.abs(x); } + function toWord(y: uint256) returns (word) { return Typedef.rep(y); } +} + +impl Mul { + function mul(x: uint256, y: uint256) returns (uint256) { + return Int.fromWord(Mul.mul(Int.toWord(x), Int.toWord(y))); + } +} +impl Int { + function fromWord(x: word) returns (word) { return x; } + function toWord(y: word) returns (word) { return y; } +} + +function bitAnd(x: word, y: word) returns (comptime word) { + let res: word; + assembly { + res := and(x,y) + } + return res; +} +function fromLit(x: word) returns (a) where a: Num { return Num.fromWord(x); } + +contract FromInt { + function main() returns (uint256) { + let k = fromLit(40); + return k+2; + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromLit.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromLit.solc new file mode 100644 index 00000000..38644466 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/comptime/fromLit.solc @@ -0,0 +1,35 @@ +import * as std from std; + +trait FromLit { + function fromLit(l: b) returns (a) ; +} + +function fromLit(l: b) returns (a) where a: FromLit { return FromLit.fromLit(l); } + +impl FromLit { + function fromLit(l: word) returns (word) { return l; } +} + +impl FromLit { + function fromLit(l: word) returns (uint256) { return uint256.uint256(l); } +} + +/* +// this does not define instance uint256:fromLit(uint256) +forall a. +default instance a:FromLit(a) { + function fromLit(l:a) -> a { l } +} +*/ +impl Mul { + function mul(a: uint256, b: uint256) returns (uint256) { return uint256.uint256(Mul.mul(Typedef.rep(a),Typedef.rep(b))); } +} + +function main() returns (uint256) { + let a: uint256 = fromLit(1); + let comptime b: uint256 = fromLit(2 + 2); // CTE + let c: uint256 = fromLit(3) + fromLit(3); // RTE + let comptime d: word = fromLit(keccakLit("foo"+"bar")); // CTE + + return b*b - fromLit(4)*a*c + fromLit(d); // RTE in RTC +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/dispatch/fib.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/dispatch/fib.solc new file mode 100644 index 00000000..0f67b3de --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/dispatch/fib.solc @@ -0,0 +1,12 @@ +import std.dispatch; + +function fib(n: word) returns (word) { + if(n < 2) { return n; } else {return fib(n-1) + fib(n-2); } +} + +contract Fib { + constructor() {} + function test() public returns (uint256) { + return uint256.uint256(fib(10)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/021nid.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/021nid.solc new file mode 100644 index 00000000..d5da89d8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/021nid.solc @@ -0,0 +1,15 @@ +contract Id1 { + function id(x) public { + return x ; + } + + function nid() public { + return id; + } + + function const(x, y) public { return x; } + + function main() public { + return nid(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/022nid-invoke.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/022nid-invoke.solc new file mode 100644 index 00000000..a6c66980 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/022nid-invoke.solc @@ -0,0 +1,37 @@ + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; + } + + function id(x) { + return x ; + } + + enum IdToken { IdToken } + +impl Invokable, a, a> { + function invoke(token: IdToken, arg: a) returns (a) { + return id(arg); + } +} + +contract InvokeId { + function id(x) public { + return x ; + } + + /* + function nid() { + return id; + } + */ + + function nidimpl() public { + return IdToken.IdToken; + } + + function main() public { + // Instead of: `return nid(42)` + return invoke(nidimpl(), 42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/024lamid.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/024lamid.solc new file mode 100644 index 00000000..a2e181c0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/024lamid.solc @@ -0,0 +1,11 @@ +contract Id1 { + function id(x) public { + return x ; + } + + + function main() public { + let nid = lam (x) {return x;}; + return nid(42); + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/025lamid-invoke.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/025lamid-invoke.solc new file mode 100644 index 00000000..3e87d310 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/025lamid-invoke.solc @@ -0,0 +1,30 @@ +/* Manual translation of: +contract Id1 { + function main() { + let nid = lam(x) {return x;}; + return nid(42); + } +} +*/ + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; +} + +function lam0impl(x: c) returns (c) { return x; } + +enum Lam0Token { Lam0Token } + +impl Invokable, a, a> { + function invoke(token: Lam0Token, arg: a) returns (a) { + return lam0impl(arg); + } +} + + +contract InvokeLam { +function main() public { + let nid = Lam0Token.Lam0Token; + return invoke(nid, 42); +} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/026capture.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/026capture.solc new file mode 100644 index 00000000..99cb2213 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/026capture.solc @@ -0,0 +1,45 @@ +/* Manual translation of: +contract Id1 { + function main() { + let y = 42; + let nid = lam(x) {return addW(x,y);}; + return nid(17); + } +} +*/ + +function addW(x: Word, y: Word) returns (Word) { + let res: Word; + assembly { + res := add(x, y) + } + return res; +} + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; +} + +// env might be a tuple, here it is a single Word +function lam1impl(env: Word, x: c) returns (c) { + let y = env; + return addW(x,y); +} + +enum Lam1Closure { Lam1Closure(Word) } + +impl Invokable, a, Word> { + function invoke(clos: Lam1Closure, arg: a) returns (Word) { + match (clos) { case Lam1Closure.Lam1Closure(env) { return lam1impl(env, arg); } } + } +} + + +contract InvokeCapLam { +function main() public { + let y = 42; + let clos = Lam1Closure.Lam1Closure(y); + + return invoke(clos, 17); +} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/027retfun.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/027retfun.solc new file mode 100644 index 00000000..8a5e1a45 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/027retfun.solc @@ -0,0 +1,42 @@ +/* Manual translation of: +contract Id1 { + + function foo() { + let y = 42; + let nid = lam(x) {return y;}; + return nid; + } + function main() { + return nid(17); + } +} +*/ + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; +} + +// env might be a tuple, here it is a single Word +function lam1impl(env: Word, x: c) returns (c) { return env; } + +enum Lam1Closure { Lam1Closure(Word) } + +impl Invokable, a, Word> { + function invoke(clos: Lam1Closure, arg: a) returns (Word) { + match (clos) { case Lam1Closure.Lam1Closure(env) { return lam1impl(env, arg); } } + } +} + + +contract InvokeCapLam { +function foo() public { + let y = 42; + let clos = Lam1Closure.Lam1Closure(y); + return clos; +} + +function main() public { + + return invoke(foo(), 17); +} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/028modifier.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/028modifier.solc new file mode 100644 index 00000000..fbc3555d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/028modifier.solc @@ -0,0 +1,85 @@ +function add1(x) { + return addW(x,1); +} + +function addW(x: Word, y: Word) returns (Word) { + let res: Word; + assembly { + res := add(x, y) + } + return res; +} + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; +} + + +/* Manual translation of: +contract Id1 { + // modifier calls its argument and adds one to result + function add1mod(f) { + return lam(a) { return add1(f(a)); } + } + + function foo(x) { + return addW(x,2); + } + + function main() { + let bar = add1mod(foo); + return bar(39); + } +} +*/ + +function foo(x: Word) returns (Word) { + return addW(x, 2); +} + +enum FooToken { FooToken } + +impl Invokable { + function invoke(self: FooToken, arg: Word) returns (Word) { + return foo(arg); + } +} + +// lambda in add1mod captures a function +// so env contains the closure + +function lam1impl(env: f, a: Word) where f: Invokable { + let f = env; + return add1(invoke(f, a)); +} + +// we want: +// data Lam1Closure = f:Invokable(Word,Word) => Lam1Closure(f) + +enum Lam1Closure { Lam1Closure(f) } + +/* +function extractEnv(clos: Lam1Closure(f)) -> f { + match clos { + | Lam1Closure(env) => return env; + }; +} +*/ +impl Invokable, Word, Word> where f: Invokable { + function invoke(clos, arg: Word) returns (Word) { + match (clos) { case Lam1Closure.Lam1Closure(env) { return lam1impl(env, arg); } } + } +} + +function add1mod(f) { + return Lam1Closure.Lam1Closure(f); +} + +contract Modifier { + + +function main() public { + let barClos = add1mod(FooToken.FooToken); + return invoke(barClos, 39); +} +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/031enum.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/031enum.solc new file mode 100644 index 00000000..06353931 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/invokable/031enum.solc @@ -0,0 +1,52 @@ +function addW(x: Word, y: Word) returns (Word) { + let res: Word; + assembly { + res := add(x, y) + } + return res; +} + +trait Enum { + function fromEnum(x: a) returns (Word) ; + } + + enum Color { R, G, B } + +impl Enum { + function fromEnum(c: Color) returns (Word) { + match (c) { case Color.R { return 1; } case Color.G { return 2; } case Color.B { return 3; } } + } +} + +enum Bool { False, True } + +impl Enum { + function fromEnum(b: Bool) returns (Word) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } + } +} +enum FromEnumToken { FromEnumToken } + +trait Invokable { + function invoke(s: self, a: args) returns (ret) ; +} + +impl Invokable, a, Word> where a: Enum { + function invoke(fet: FromEnumToken, arg) returns (Word) { + return fromEnum(arg); + } +} +contract RGB { + function main() public { + /* + let x = fromEnum(Color.B); + let y = fromEnum(Bool.True); + */ + + let fetC = FromEnumToken.FromEnumToken; + let fetB = FromEnumToken.FromEnumToken; + let x = invoke(fetC, Color.B); + let y = invoke(fetB,Bool.True); + return addW(x,y); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/pragmas/bound.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/pragmas/bound.solc new file mode 100644 index 00000000..59f16853 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/pragmas/bound.solc @@ -0,0 +1,15 @@ + +trait D { function f(x: a); } +trait F {} + +enum Memory { Memory(word) } + +impl F, Memory>>> {} +impl D>>> where c: D, a: F { + function f(x: Memory>>) {} +} + +function g(y: b) { + let x: Memory>>>; + f(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/010answer.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/010answer.solc new file mode 100644 index 00000000..5681aed3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/010answer.solc @@ -0,0 +1,5 @@ +contract Answer { + function main() public { + return 42; + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/011id.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/011id.solc new file mode 100644 index 00000000..cc2f792e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/011id.solc @@ -0,0 +1,14 @@ +contract Id1 { + + enum Bool { False, True } + + function id(x) public { + return x ; + } + + function const(x, y) public { return x; } + + function main() public { + return const(id(42), Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/012nid.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/012nid.solc new file mode 100644 index 00000000..e2a17171 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/012nid.solc @@ -0,0 +1,15 @@ +contract Id1 { + function id(x) public { + return x ; + } + + function nid() public { + return id; + } + + function const(x, y) public { return x; } + + function main() public { + return const(nid(42), id(1)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/013comp.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/013comp.solc new file mode 100644 index 00000000..26305393 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/013comp.solc @@ -0,0 +1,16 @@ +contract Compose { + function compose(f, g) public { + return lam (x) { + return f(g(x)); + } ; + } + + function id(x) public { return x; } + + function idid() public { return compose(id,id); } + + function main() public { + let f = compose(id,id); + return f(42); + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/027sstore.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/027sstore.solc new file mode 100644 index 00000000..c4d1b106 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/027sstore.solc @@ -0,0 +1,10 @@ +contract Sstore { + function main() public { + let res: word; + assembly { + sstore(0, 42) + res := sload(0) + } + return res; + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051expreturn.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051expreturn.solc new file mode 100644 index 00000000..da8de61d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051expreturn.solc @@ -0,0 +1,53 @@ +enum Bool { False, True } +enum W { W(Word) } +enum U { U } + +// empty class needed since forall expects a nonempty context +trait Top {} +impl Top {} + +/* For experiments, special handling when emitting code */ +// this does not work, typechecker forces a ~ b +// forall a, b.(a:Top, b:Top) => function ereturn(x:a) -> b { let res: b; return res; } +// we might have +// forall a.(a:Top) => function ereturn(x:a) -> a +// or + +function ereturn(x: a) returns (Unit) { let res: Unit; return res; } +// and then cast it to any type using unsafeCast + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (Word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = unsafeCast(ereturn(ereturn(77))); + // but this does not + // | Bool.False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + +// "semicolon" +function semi(x: a) returns (U) { return U.U;} + +function unsafeCast(x: a) returns (b) { + let res: b; return res; +} + + +contract ExpReturn { + function main() public returns (Word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051negBool.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051negBool.solc new file mode 100644 index 00000000..1dd66c3f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/051negBool.solc @@ -0,0 +1,23 @@ + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } + + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + + +contract NegBool { + + function fromB(b) public { + match (b) { case B.F { return 0; } case B.T { return 1; } } + } + + function main() public { return fromB(Neg.neg(B.F)); } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052negPair.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052negPair.solc new file mode 100644 index 00000000..51624e6a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052negPair.solc @@ -0,0 +1,50 @@ + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } +enum Pair { Pair(a, b) } + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + +function fst(p) { + match (p) { case Pair.Pair(x, y) { return x; } } +} + +function snd(p) { + match (p) { case Pair.Pair(x, y) { return y; } } +} + + +impl Neg> where a: Neg, b: Neg { + function neg(p: Pair) returns (Pair) { + return Pair.Pair(Neg.neg (fst(p)), Neg.neg(snd (p))); + } +} + +/* +instance (a:Neg,b:Neg) => Pair(a,b):Neg { + function neg(p) { + match p { + | Pair(a,b) => return Pair(neg(a), neg(b)); + } + } +} +*/ +contract NegPair { + + function bnot(x) public { + match (x) { case B.T { return B.F; } case B.F { return B.T; } } +} + + function fromB(b) public { + match (b) { case B.F { return 0; } case B.T { return 1; } } +} + + function main() public { return fromB(fst(Neg.neg(Pair.Pair(B.F,B.T)))); } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052return.solc new file mode 100644 index 00000000..b74d46cf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/052return.solc @@ -0,0 +1,57 @@ +enum Bool { False, True } +enum W { W(word) } +enum U { U } + + +/* For experiments, special handling when emitting code */ +// this does not work, typechecker forces a ~ b +// function ereturn(x:a) -> b { let res: b; return res; } +// we might have +// function ereturn(x:a) -> a +// or + +function ereturn(x: a) returns (unit) { let res: unit; return res; } +// and then cast it to any type using unsafeCast + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = unsafeCast(ereturn(77)); + // but this does not - unknown intermediate type + // | Bool.False => x = unsafeCast(unsafeCast(ereturn(77))); + // what about "return(return 77)"? + // this does not work + // | Bool.False => x = ereturn(ereturn(77)); + // this works + // | Bool.False => x = unsafeCast(ereturn(ereturn(77))); + // this does not work (monomorphisation fails): + // | Bool.False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + +// "semicolon" +function semi(x: a) returns (U) { return U.U;} + +function unsafeCast(x: a) returns (b) { +let res: b; return res; +} + + +contract ExpReturn { + function main() public returns (word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.True); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/053return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/053return.solc new file mode 100644 index 00000000..ae357efd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/053return.solc @@ -0,0 +1,36 @@ +enum Bool { False, True } +enum W { W(word) } + + +/* For experiments, special handling when emitting code */ +function ereturn(x: a) returns (b) { let res: b; return res; } + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = ereturn(77); + // what about "return(return 77)"? + // this does not work (monomorphisation fails) + // | Bool.False => x = ereturn(ereturn(77)); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + + +contract ExpReturn { + function main() public returns (word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.True); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/101struct1Field.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/101struct1Field.solc new file mode 100644 index 00000000..18ec41e0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/101struct1Field.solc @@ -0,0 +1,241 @@ + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +impl Typedef { + function rep(x: word) returns (word) { return x; } + function abs(x: word) returns (word) { return x;} +} + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + + +function mload_(x: word) returns (word) { + let res: word; + assembly { + res := mload(x) + } + return res; + } + +function mstore_(a: word, v: word) { + assembly { mstore(a,v) } +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + let value: uint = Typedef.abs(mload_(ptr)); + return value; // type annotation needed due to a typechecker bug + } + function store(ptr: word, value: uint) returns () { + return mstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field, Proxy) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z, p) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +// This is *a lot* of pragmas... +// pragma no-coverage-condition CStructField, LValueMemberAccess, RValueMemberAccess; +// pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; +// pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(ptr); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return MemoryType.load(ptr); + } +} + +////// Testing + +// struct S { fld1:word; } +enum S { S(word) } +enum fld1_sel { fld1_sel } +// data y_sel = y_sel; +// data z_sel = z_sel; + +impl CStructField, word, ()> {} +// instance StructField(S, y_sel):CStructField(uint, word) {} +// BUG: This next one should really be the following, but that breaks weirdly: +// (I get a patterson condition violation on an invoke instance for g) +impl CStructField, word, (word, uint)> {} +// So instead I use: +// instance StructField(S, z_sel):CStructField(word, word) {} + + +function f() { + let x: word memory; + let y: word memory; + // x = y + Assign.assign(ref.ref(x), y); + /* + * Idea in the above: to avoid overlapping instances, + * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), + * to be able to choose a disjoint assign instance. + * Of course this needs special treatment during code generation, + * on the other hand, stack assignments generally do... + * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. + */ +} + +function g() returns (word) { + let s: S memory = Typedef.abs(0x80); + + let offset0: Proxy<()> = Proxy.Proxy; + // s.fld1 = y + let fld1_lval: memoryRef = LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel, offset0)); + Assign.assign(fld1_lval, y); + // return s.fld1 + let r: word = 17; + r = RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel, offset0) ); + return r; +} + +contract C { + function main() public { + f(); + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/102uintField.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/102uintField.solc new file mode 100644 index 00000000..eb63ccbd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/102uintField.solc @@ -0,0 +1,250 @@ + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + + +function mload_(x: word) returns (word) { + let res: word; + assembly { + res := mload(x) + } + return res; + } + +function mstore_(a: word, v: word) { + assembly { mstore(a,v) } +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + let value: uint = Typedef.abs(mload_(ptr)); + return value; // type annotation needed due to a typechecker bug + } + function store(ptr: word, value: uint) returns () { + return mstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field, Proxy) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z, p) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +// This is *a lot* of pragmas... +// pragma no-coverage-condition CStructField, LValueMemberAccess, RValueMemberAccess; +// pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; +// pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(ptr); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + let value: fieldType = MemoryType.load(ptr); + return value; + } +} + +////// Testing + +// struct S { fld1:uint; } +enum S { S(uint) } +enum fld1_sel { fld1_sel } +// data y_sel = y_sel; +// data z_sel = z_sel; + +impl CStructField, uint, ()> {} +// instance StructField(S, y_sel):CStructField(uint, uint) {} +// BUG: This next one should really be the following, but that breaks weirdly: +// (I get a patterson condition violation on an invoke instance for g) +// instance StructField(S, z_sel):CStructField(word, (word,uint)) {} +// So instead I use: +// instance StructField(S, z_sel):CStructField(word, word) {} + + +function f() { + let x: word memory; + let y: word memory; + // x = y + Assign.assign(ref.ref(x), y); + /* + * Idea in the above: to avoid overlapping instances, + * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), + * to be able to choose a disjoint assign instance. + * Of course this needs special treatment during code generation, + * on the other hand, stack assignments generally do... + * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. + */ +} + +function g() returns (word) { + let s: S memory = Typedef.abs(0x80); + // let y:word = 42; + let z: uint = uint.uint(42); + + let offset0: Proxy<()> = Proxy.Proxy; + // s.fld1 = z + let fld1_lval: memoryRef = LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel, offset0)); + Assign.assign(fld1_lval, z); + // return s.fld1 + let r: uint = uint.uint(17); + r = RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel, offset0) ); + let annotatedR: uint = r; + let r2: word = Typedef.rep(annotatedR); + return r2; +} + +contract C { + function main() public { + f(); + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/103struct3Fields.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/103struct3Fields.solc new file mode 100644 index 00000000..eefcef6b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/103struct3Fields.solc @@ -0,0 +1,273 @@ +// v4: Simplified Member AccessProxy (no Proxy(offset)) +// variables holding field MAPs + +function add(x: word, y: word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + + +function mload_(x: word) returns (word) { + let res: word; + assembly { + res := mload(x) + } + return res; + } + +function mstore_(a: word, v: word) { + assembly { mstore(a,v) } +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + let value: uint = Typedef.abs(mload_(ptr)); + return value; + } + function store(ptr: word, value: uint) returns () { + return mstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(ptr); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +/* +// fails Patterson cond +forall a b . a:Typedef(b), b:MemorySize +=> instance a:MemorySize { + function size(x:Proxy(a)) -> word { + return MemorySize.size(Proxy(b)); + } +} +*/ + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + let value: fieldType = MemoryType.load(ptr); + return value; + } +} + +////// Testing + +// struct S { fld1:uint; fld2:word; fld3:word } +enum S { S } // (uint, word, word); +enum fld1_sel { fld1_sel } +enum fld2_sel { fld2_sel } +enum fld3_sel { fld3_sel } + +// form: +// instance StructField(S, f_sel):CStructField(ftype, preceding)) {} +impl CStructField, uint, ()> {} +impl CStructField, word, uint> {} +impl CStructField, word, (uint, word)> {} + + +function g() returns (word) { + let s: S memory = Typedef.abs(0x80); + let fld1_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel); + let fld2_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld2_sel.fld2_sel); + let fld3_map: MemberAccessProxy = + MemberAccessProxy.MemberAccessProxy(s, fld3_sel.fld3_sel); + // let y:word = 13; + let z: uint = uint.uint(13); + + // s.fld1 = z + + let fld1_lval: memoryRef = LValueMemberAccess.memberAccess(fld1_map ); + Assign.assign(fld1_lval, z); + + // s.fld2 = 14 + let fld2_lval // : memoryRef(word) + = LValueMemberAccess.memberAccess(fld2_map); + Assign.assign(fld2_lval, 14); + + // s.fld3 = 15 + let fld3_lval // : memoryRef(word) + = LValueMemberAccess.memberAccess(fld3_map); + Assign.assign(fld3_lval, 15); + + // let f1 = S.fld1 + let f1: uint; + f1 = RValueMemberAccess.memberAccess(fld1_map); + + let f2: word; + f2 = RValueMemberAccess.memberAccess(fld2_map); + + let f3: word; + f3 = RValueMemberAccess.memberAccess(fld3_map); + + let f1Rep: word = Typedef.rep(f1); + let f12 = add(f1Rep, f2); + let f123 = add(f12, f3); + + return f123; + +} + +contract C { + function main() public { + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/105nestedStruct.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/105nestedStruct.solc new file mode 100644 index 00000000..1fe733ab --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/105nestedStruct.solc @@ -0,0 +1,329 @@ +// v5: nested struct +// variables holding field MAPs + +function add(x: word, y: word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + + +function mload_(x: word) returns (word) { + let res: word; + assembly { + res := mload(x) + } + return res; + } + +function mstore_(a: word, v: word) { + assembly { mstore(a,v) } +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + return Typedef.abs(mload_(ptr)); + } + function store(ptr: word, value: uint) returns () { + return mstore_(ptr, Typedef.rep(value)); + } +} + +impl MemoryType { + function load(ptr: word) returns (a memory) { + return Typedef.abs(mload_(ptr)); + } + function store(ptr: word, value: a memory) returns () { + return mstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(ptr); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +/* +// fails Patterson cond +forall a b . a:Typedef(b), b:MemorySize +=> instance a:MemorySize { + function size(x:Proxy(a)) -> word { + return MemorySize.size(Proxy(b)); + } +} +*/ + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + let value: fieldType = MemoryType.load(ptr); + return value; + } +} + +////// Testing + +// struct S { fld1:uint; fld2:word; fld3:word } +enum S { S } // (uint, word, word); + +// struct W { flds : memory(W) } +enum W { W } + +enum fld1_sel { fld1_sel } +enum fld2_sel { fld2_sel } +enum fld3_sel { fld3_sel } + +enum flds_sel { flds_sel } + +// form: +// instance StructField(S, f_sel):CStructField(ftype, preceding)) {} +impl CStructField, uint, ()> {} +impl CStructField, word, uint> {} +impl CStructField, word, (uint, word)> {} + +impl CStructField, S memory, ()> {} + +function makeS() returns (S memory) { + let s: S memory = Typedef.abs(0x80); + let fld1_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel); + let fld2_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld2_sel.fld2_sel); + let fld3_map: MemberAccessProxy = + MemberAccessProxy.MemberAccessProxy(s, fld3_sel.fld3_sel); + // let y:word = 13; + let z: uint = uint.uint(13); + + // s.fld1 = z + + let fld1_lval: memoryRef = LValueMemberAccess.memberAccess(fld1_map ); + Assign.assign(fld1_lval, z); + + // s.fld2 = 14 + let fld2_lval // : memoryRef(word) + = LValueMemberAccess.memberAccess(fld2_map); + Assign.assign(fld2_lval, 14); + + // s.fld3 = 15 + let fld3_lval // : memoryRef(word) + = LValueMemberAccess.memberAccess(fld3_map); + Assign.assign(fld3_lval, 15); + return s; +} + +function readS(s: S memory) returns (word) { + let s: S memory = Typedef.abs(0x80); + let fld1_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel); + let fld2_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld2_sel.fld2_sel); + let fld3_map: MemberAccessProxy = + MemberAccessProxy.MemberAccessProxy(s, fld3_sel.fld3_sel); + + // let f1 = s.fld1 + let f1: uint; + f1 = RValueMemberAccess.memberAccess(fld1_map); + + // let f2 = s.fld2 + let f2: word; + f2 = RValueMemberAccess.memberAccess(fld2_map); + + let f3: word; + f3 = RValueMemberAccess.memberAccess(fld3_map); + + let f1Rep: word = Typedef.rep(f1); + let f12 = add(f1Rep, f2); + let f123 = add(f12, f3); + + return f123; +} + +function rwS() returns (word) { + let s: S memory = makeS(); + return readS(s); + +} + + +function makeW(s: S memory) returns (W memory) { + let w: W memory = Typedef.abs(0xe0); + let flds_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(w, flds_sel.flds_sel); + + // w.flds = s + let flds_lval: memoryRef = LValueMemberAccess.memberAccess(flds_map ); + Assign.assign(flds_lval, s); + + return w; +} + +function readW(w: W memory) returns (S memory) { + let flds_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(w, flds_sel.flds_sel); + return RValueMemberAccess.memberAccess(flds_map); +} + +contract C { + function main() public { + let s: S memory = makeS(); + let w: W memory = makeW(s); + let s2: S memory = readW(w); + return readS(s2); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/111storageStruct.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/111storageStruct.solc new file mode 100644 index 00000000..7c51b11a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/111storageStruct.solc @@ -0,0 +1,277 @@ +// v4: Simplified Member AccessProxy (no Proxy(offset)) +// variables holding field MAPs + +function add(x: word, y: word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum storage { storage(word) } +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a storage) returns (word) { + match (x) { case storage.storage(y) { return y; } } + } + function abs(x: word) returns (a storage) { + return storage.storage(x); + } +} +impl Typedef, word> { + function rep(x: storageRef) returns (word) { + match (x) { case storageRef.storageRef(y) { return y; } } + } + function abs(x: word) returns (storageRef) { + return storageRef.storageRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +/* +data ref(a) = ref(a); + +instance ref(a):Assign(a) { + function assign(l:ref(a), r:a) -> () { + // builtin "stack store" + return (); + } +} +*/ + +trait StorageType { + function sload(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + + +function sload_(x: word) returns (word) { + let res: word; + assembly { + res := sload(x) + } + return res; + } + +function sstore_(a: word, v: word) { + assembly { sstore(a,v) } +} + +impl StorageType { + function sload(ptr: word) returns (word) { + let r: word; + assembly { + r := sload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + sstore(ptr, value) + } + } +} + +impl StorageType { + function sload(ptr: word) returns (uint) { + let value: uint = Typedef.abs(sload_(ptr)); + return value; // type annotation needed due to a typechecker bug + } + function store(ptr: word, value: uint) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: StorageType { + function assign(l: storageRef, y: a) { + StorageType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, storageRef> where StructField: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = StorageSize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return storageRef.storageRef(ptr); + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + + +/* +// fails Patterson cond +forall a b . a:Typedef(b), b:StorageSize +=> instance a:StorageSize { + function size(x:Proxy(a)) -> word { + return StorageSize.size(Proxy(b)); + } +} +*/ + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: StorageType, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = StorageSize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + let value: fieldType = StorageType.sload(ptr); + return value; + } +} + +// helpers + +////// Testing + +// struct S { fld1:uint; fld2:word; fld3:word } +enum S { S } // (uint, word, word); +enum fld1_sel { fld1_sel } +enum fld2_sel { fld2_sel } +enum fld3_sel { fld3_sel } + +// form: +// instance StructField(S, f_sel):CStructField(ftype, preceding)) {} +impl CStructField, uint, ()> {} +impl CStructField, word, uint> {} +impl CStructField, word, (uint, word)> {} + + +function g() returns (word) { + let s: S storage = Typedef.abs(0x80); + let fld1_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld1_sel.fld1_sel); + let fld2_map: MemberAccessProxy = MemberAccessProxy.MemberAccessProxy(s, fld2_sel.fld2_sel); + let fld3_map: MemberAccessProxy = + MemberAccessProxy.MemberAccessProxy(s, fld3_sel.fld3_sel); + // let y:word = 13; + let z: uint = uint.uint(13); + + // s.fld1 = z + + let fld1_lval: storageRef = LValueMemberAccess.memberAccess(fld1_map ); + Assign.assign(fld1_lval, z); + + // s.fld2 = 14 + let fld2_lval // : storageRef(word) + = LValueMemberAccess.memberAccess(fld2_map); + Assign.assign(fld2_lval, 14); + + // s.fld3 = 15 + let fld3_lval // : storageRef(word) + = LValueMemberAccess.memberAccess(fld3_map); + Assign.assign(fld3_lval, 15); + + // let f1 = S.fld1 + let f1: uint; + f1 = RValueMemberAccess.memberAccess(fld1_map); + + let f2: word; + f2 = RValueMemberAccess.memberAccess(fld2_map); + + let f3: word; + f3 = RValueMemberAccess.memberAccess(fld3_map); + + let f1Rep: word = Typedef.rep(f1); + let f12 = add(f1Rep, f2); + let f123 = add(f12, f3); + + return f123; + +} + +contract C { + function main() public { + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/112ContractStorage.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/112ContractStorage.solc new file mode 100644 index 00000000..d81f9a2e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/112ContractStorage.solc @@ -0,0 +1,42 @@ +import * as StorageLib from StorageLib; + +/* +// Translating contract: +contract Counter { + counter : word; + + function main() -> word { + counter = add(counter, 1); + return counter; + } +} +*/ + + + +// form: +// instance StructField(S, f_sel):CStructField(ftype, preceding)) {} +enum CounterCxt { CounterCxt } +enum counter_sel { counter_sel } +impl CStructField, counter_sel>, word, ()> {} + +contract Counter { + // struct CounterCxt { counter:word } + + function main() public returns (word) { + let cxt: ContractStorage = ContractStorage.ContractStorage(CounterCxt.CounterCxt); + let counter_map: MemberAccessProxy, counter_sel, ()> = MemberAccessProxy.MemberAccessProxy(cxt, counter_sel.counter_sel); + + // let c1 = this.counter + let c1: word; + c1 = RValueMemberAccess.memberAccess(counter_map); + + // this.counter = c1 + 7 + let counter_lval // : storageRef(word) + = LValueMemberAccess.memberAccess(counter_map); + + Assign.assign(counter_lval, add(c1, 7)); + + return RValueMemberAccess.memberAccess(counter_map); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/113counter.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/113counter.solc new file mode 100644 index 00000000..34507513 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/113counter.solc @@ -0,0 +1,23 @@ +import * as StorageLib from StorageLib; + +/* +contract Counter { + counter : word; + + function main() -> word { + counter = add(counter, 1); + return counter; + } +} +*/ + +enum counter_sel { counter_sel } +impl CStructField, counter_sel>, word, ()> {} + +contract Counter { + function main() public returns (word) { + let counter_map /*: MemberAccessProxy(ContractStorage(()), counter_sel, ()) */ = MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(()), counter_sel.counter_sel); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(()), counter_sel.counter_sel)), add(rval(counter_map), 1)); + return rval(counter_map); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/131constructor.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/131constructor.solc new file mode 100644 index 00000000..ab26a3ff --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/131constructor.solc @@ -0,0 +1,27 @@ +// test constructor + +contract Counter { + + function setCounter(v: word) public { + assembly { + sstore(0x00, v) + } + } + + function getCounter() public returns (word) { + let res; + assembly { + res := sload(0x00) + } + return res; + } + + + constructor() { + setCounter(42); + } + + function main() public returns (word) { + return getCounter(); + } +} diff --git a/crates/parser/tests/fixtures/ok/spec/135cons3.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/135cons3.solc similarity index 83% rename from crates/parser/tests/fixtures/ok/spec/135cons3.solc rename to crates/parser/tests/fixtures/corpus/fail/test/examples/spec/135cons3.solc index 07d91117..09a9adf2 100644 --- a/crates/parser/tests/fixtures/ok/spec/135cons3.solc +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/135cons3.solc @@ -3,9 +3,8 @@ import std; // import prelude; -forall t.t:Typedef(word) => -function log1(v:t, topic:word) -> () { - let w : word = Typedef.rep(v); +function log1(v: t, topic: word) returns () where t: Typedef { + let w: word = Typedef.rep(v); assembly { mstore(0,w) log1(0,32,topic) @@ -15,25 +14,21 @@ function log1(v:t, topic:word) -> () { contract Counter { // setCounter & getCounter are intentionally low-level to avoid clutter - function setCounter(v: uint256) -> () { - match v { | uint256(w) => - assembly { + function setCounter(v: uint256) public returns () { + match (v) { case uint256.uint256(w) { assembly { sstore(0x00, w) - } - } + } } } } - function getCounter() -> uint256 { + function getCounter() public returns (uint256) { let res; assembly { res := sload(0x00) } - return uint256(res); + return uint256.uint256(res); } - constructor(x:uint256, y:uint256, z:uint256) - // function myconstructor(x:uint256, y:uint256, z:uint256) -> () - { + constructor(x: uint256, y: uint256, z: uint256) { log1(x, 0xc1); log1(y, 0xc2); log1(z, 0xc3); @@ -90,7 +85,7 @@ contract Counter { */ // TODO: remove main, use dispatch instead - function main() -> uint256 { + function main() returns (uint256) { return getCounter(); } diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/StorageLib.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/StorageLib.solc new file mode 100644 index 00000000..8837619a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/StorageLib.solc @@ -0,0 +1,230 @@ +// v4: Simplified Member AccessProxy (no Proxy(offset)) +// variables holding field MAPs + +function add(x: word, y: word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum storage { storage(word) } +enum ContractStorage { ContractStorage(cxt) } + +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a storage) returns (word) { + match (x) { case storage.storage(y) { return y; } } + } + function abs(x: word) returns (a storage) { + return storage.storage(x); + } +} + +impl Typedef, word> { + function rep(x: storageRef) returns (word) { + match (x) { case storageRef.storageRef(y) { return y; } } + } + function abs(x: word) returns (storageRef) { + return storageRef.storageRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait StorageType { + function sload(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + + +function sload_(x: word) returns (word) { + let res: word; + assembly { + res := sload(x) + } + return res; + } + +function sstore_(a: word, v: word) { + assembly { sstore(a,v) } +} + +impl StorageType { + function sload(ptr: word) returns (word) { + let r: word; + assembly { + r := sload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + sstore(ptr, value) + } + } +} + +impl StorageType { + function sload(ptr: word) returns (uint) { + let value: uint = Typedef.abs(sload_(ptr)); + return value; // type annotation needed due to a typechecker bug + } + function store(ptr: word, value: uint) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: StorageType { + function assign(l: storageRef, y: a) returns () { + StorageType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +trait CStructField {} + +enum StructField { StructField(structType) } + +impl LValueMemberAccess, storageRef> where StructField: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = StorageSize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return storageRef.storageRef(ptr); + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + + +/* +// fails Patterson cond +forall a b . a:Typedef(b), b:StorageSize +=> instance a:StorageSize { + function size(x:Proxy(a)) -> word { + return StorageSize.size(Proxy(b)); + } +} +*/ + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +pragma solcore noPattersonCondition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances +pragma solcore noCoverageCondition LValueMemberAccess, RValueMemberAccess; + +impl LValueMemberAccess, fieldSelector, offsetType>, storageRef> where StructField, fieldSelector>: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (storageRef) { + let ptr: word = 0x100; // forge uses at least 1 storage slot + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy); + + assembly { + ptr := add(ptr, offsetSize) + } + return storageRef.storageRef(ptr); // contract storage starts at 0 + } +} + +impl RValueMemberAccess, fieldSelector, offsetType>, fieldType> where StructField, fieldSelector>: CStructField, fieldType: StorageType, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (fieldType) { + let ptr: word = 0x100; + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy); + let value: fieldType = StorageType.sload(add(ptr, offsetSize)); + return value; + } +} + +function rval(x: a) returns (b) where a: RValueMemberAccess { + return RValueMemberAccess.memberAccess(x); +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/051expreturn.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/051expreturn.solc new file mode 100644 index 00000000..58fa4e5b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/051expreturn.solc @@ -0,0 +1,56 @@ +enum Bool { False, True } +enum W { W(Word) } +enum U { U } + +// empty class needed since forall expects a nonempty context +trait Top {} +impl Top {} + +/* For experiments, special handling when emitting code */ +// this does not work, typechecker forces a ~ b +// forall a, b.(a:Top, b:Top) => function ereturn(x:a) -> b { let res: b; return res; } +// we might have +// forall a.(a:Top) => function ereturn(x:a) -> a +// or + +function ereturn(x: a) returns (Unit) where a: Top { let res: Unit; return res; } +// and then cast it to any type using unsafeCast + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (Word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = unsafeCast(ereturn(ereturn(77))); + // but this does not + // | Bool.False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + +// "semicolon" +function semi(x: a) returns (U) where a: Top { return U.U;} + +function unsafeCast(x: a) returns (b) where a: Top, b: Top { + let res: b; return res; +} + + +contract ExpReturn { + + + + function main() public returns (Word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/052return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/052return.solc new file mode 100644 index 00000000..b74d46cf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/052return.solc @@ -0,0 +1,57 @@ +enum Bool { False, True } +enum W { W(word) } +enum U { U } + + +/* For experiments, special handling when emitting code */ +// this does not work, typechecker forces a ~ b +// function ereturn(x:a) -> b { let res: b; return res; } +// we might have +// function ereturn(x:a) -> a +// or + +function ereturn(x: a) returns (unit) { let res: unit; return res; } +// and then cast it to any type using unsafeCast + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = unsafeCast(ereturn(77)); + // but this does not - unknown intermediate type + // | Bool.False => x = unsafeCast(unsafeCast(ereturn(77))); + // what about "return(return 77)"? + // this does not work + // | Bool.False => x = ereturn(ereturn(77)); + // this works + // | Bool.False => x = unsafeCast(ereturn(ereturn(77))); + // this does not work (monomorphisation fails): + // | Bool.False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + +// "semicolon" +function semi(x: a) returns (U) { return U.U;} + +function unsafeCast(x: a) returns (b) { +let res: b; return res; +} + + +contract ExpReturn { + function main() public returns (word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.True); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/053return.solc b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/053return.solc new file mode 100644 index 00000000..ae357efd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/examples/spec/attic/053return.solc @@ -0,0 +1,36 @@ +enum Bool { False, True } +enum W { W(word) } + + +/* For experiments, special handling when emitting code */ +function ereturn(x: a) returns (b) { let res: b; return res; } + +/* simulate match expression + x = match { | Bool.False => return 77; | Bool.True => W(22) } +*/ +function elimBool1(b: Bool) returns (word) { + let x: W; + x = W.W(1); + match (b) { + case Bool.False { + x = ereturn(77); + // what about "return(return 77)"? + // this does not work (monomorphisation fails) + // | Bool.False => x = ereturn(ereturn(77)); + } + case Bool.True { + x = W.W(22); + } + } + + match (x) { case W.W(y) { return y; } } + +} + + +contract ExpReturn { + function main() public returns (word) { + return elimBool1(Bool.False); + // return elimBool1(Bool.True); + } +} diff --git a/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.snap b/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.snap new file mode 100644 index 00000000..e5f80a7e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.snap @@ -0,0 +1,15 @@ +--- +source: crates/parser/tests/diagnostics.rs +expression: value +input_file: crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.solc +--- +error[SC0001]: parse error: unexpected `as` + --> /select_alias_tail_fail.solc:1:30 + | +1 | import {keep} from selectlib as keep_; + | ^^ unexpected token +2 | +3 | function main(x: word) returns (word) { + | + = note: expecting `.`, or `;` + = note: while parsing import declaration diff --git a/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.solc b/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.solc new file mode 100644 index 00000000..db1f87ae --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/fail/test/imports/select_alias_tail_fail.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib as keep_; + +function main(x: word) returns (word) { + return keep_(x); +} diff --git a/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/duplicate-definition.solc b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/duplicate-definition.solc new file mode 100644 index 00000000..ea0d4cc1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/duplicate-definition.solc @@ -0,0 +1,3 @@ +function foo() returns (word) { return 1; } +function foo() returns (word) { return 2; } +function main() returns (word) { return foo(); } diff --git a/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/missing-signature.solc b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/missing-signature.solc new file mode 100644 index 00000000..059ca49d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/missing-signature.solc @@ -0,0 +1,3 @@ +function foo() { + return 1; +} diff --git a/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/not-polymorphic-enough.solc b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/not-polymorphic-enough.solc new file mode 100644 index 00000000..9411dc19 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/not-polymorphic-enough.solc @@ -0,0 +1,5 @@ +function fromWord(x: word) returns (a) { + let result; + assembly { result := x } + return result; +} diff --git a/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/type-mismatch.solc b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/type-mismatch.solc new file mode 100644 index 00000000..2ca138c6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/type-mismatch.solc @@ -0,0 +1 @@ +function main() returns (word) { return true; } diff --git a/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/undefined-name.solc b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/undefined-name.solc new file mode 100644 index 00000000..db2d49cd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/known-diagnostic-gaps/test/diagnostics/undefined-name.solc @@ -0,0 +1 @@ +function main() returns (word) { return missing; } diff --git a/crates/parser/tests/fixtures/corpus/ok/std/ABIGeneric.solc b/crates/parser/tests/fixtures/corpus/ok/std/ABIGeneric.solc new file mode 100644 index 00000000..a0a2442f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/std/ABIGeneric.solc @@ -0,0 +1,95 @@ +pragma solcore noPattersonCondition ABIAttribs, ABIEncode, ABIDecode; +pragma solcore noBoundVariableCondition ABIAttribs, ABIEncode, ABIDecode; +pragma solcore noCoverageCondition ABIDecode; + +export { + encode, + decode +}; + +import std; +import {mstore} from std.opcodes; +import std.Generic; + +function maxWord(a: word, b: word) returns (word) { + match (gtWord(a, b)) { case true { return a; } case false { return b; } } +} + +// ─── ABIAttribs for the primitive sum(f, g) type ───────────────────────── +// headSize = 32 (tag word) + max(headSize(f), headSize(g)) + +impl ABIAttribs> where f: ABIAttribs, g: ABIAttribs { + function headSize(ty: Proxy>) returns (word) { + let pf: Proxy; + let pg: Proxy; + return 32 + maxWord(ABIAttribs.headSize(pf), ABIAttribs.headSize(pg)); + } + function isStatic(ty: Proxy>) returns (bool) { + let pf: Proxy; + let pg: Proxy; + return and(ABIAttribs.isStatic(pf), ABIAttribs.isStatic(pg)); + } +} + +// ─── ABIEncode for sum(f, g) ───────────────────────────────────────────── +// Wire layout (static sums only): +// [offset + 0 .. offset + 31] : tag word (0 = inl, 1 = inr) +// [offset + 32 .. ] : encoded branch payload + +impl ABIEncode> where f: ABIAttribs, f: ABIEncode, g: ABIAttribs, g: ABIEncode { + function encodeInto(x: sum, basePtr: word, offset: word, tail: word) returns (word) { + match (x) { case inl(v) { mstore(basePtr + offset, 0); + return ABIEncode.encodeInto(v, basePtr, offset + 32, tail); } case inr(v) { mstore(basePtr + offset, 1); + return ABIEncode.encodeInto(v, basePtr, offset + 32, tail); } } + } +} + +// ─── ABIDecode for sum(f, g) ───────────────────────────────────────────── +// Reads the tag word at headOffset; dispatches to f or g decoder at headOffset + 32. + +impl ABIDecode, reader>, sum> where reader: WordReader, f: ABIAttribs, ABIDecoder: ABIDecode, ABIDecoder: ABIDecode { + function decode(ptr: ABIDecoder, reader>, headOffset: word) returns (sum) { + match (ptr) { case ABIDecoder.ABIDecoder(rdr) { let tag = WordReader.read(WordReader.advance(rdr, headOffset)); + match (tag) { case 0 { let dec_f: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return inl(ABIDecode.decode(dec_f, headOffset + 32)); } default { let dec_g: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return inr(ABIDecode.decode(dec_g, headOffset + 32)); } } } } + } +} + +// ─── Default bridges: ABIAttribs and ABIEncode via Generic ─────────────── +// Any type 'a' with Generic(rep) inherits its ABI layout from rep. + +default impl ABIAttribs where a: Generic, rep: ABIAttribs { + function headSize(ty: Proxy) returns (word) { + let prx: Proxy; + return ABIAttribs.headSize(prx); + } + function isStatic(ty: Proxy) returns (bool) { + let prx: Proxy; + return ABIAttribs.isStatic(prx); + } +} + +default impl ABIEncode where a: Generic, rep: ABIAttribs, rep: ABIEncode { + function encodeInto(x: a, basePtr: word, offset: word, tail: word) returns (word) { + return ABIEncode.encodeInto(Generic.from(x), basePtr, offset, tail); + } +} + +// ─── Top-level generic encode function ─────────────────────────────────── +// Serialises any 'a' that has a Generic impl. +// Only the Generic impl is required — ABIEncode is resolved via the bridge. + +function encode(x: a, basePtr: word, offset: word, tail: word) returns (word) where a: Generic, rep: ABIAttribs, rep: ABIEncode { + let xrep: rep = Generic.from(x); + return ABIEncode.encodeInto(xrep, basePtr, offset, tail); +} + +// ─── Top-level generic decode function ─────────────────────────────────── +// Deserialises any 'a' that has a Generic impl. +// Only the Generic impl is required — ABIDecode is resolved via the bridge. + +function decode(ptr: ABIDecoder, headOffset: word) returns (a) where a: Generic, reader: WordReader, ABIDecoder: ABIDecode { + match (ptr) { case ABIDecoder.ABIDecoder(rdr) { let rep_ptr: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return Generic.to(ABIDecode.decode(rep_ptr, headOffset)); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/std/Generic.solc b/crates/parser/tests/fixtures/corpus/ok/std/Generic.solc new file mode 100644 index 00000000..2194c5bc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/std/Generic.solc @@ -0,0 +1,16 @@ +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +export { Generic }; + +import std; + +// MPTC: isomorphism between a user type and its SOP representation. +// The representation 'rep' is built from primitive Solcore types: +// sum(f, g) with constructors inl / inr +// (f, g) pair (product) +// () unit +trait Generic { + function from(x: a) returns (rep) ; + function to(x: rep) returns (a) ; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/std/dispatch.solc b/crates/parser/tests/fixtures/corpus/ok/std/dispatch.solc new file mode 100644 index 00000000..1a99da7e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/std/dispatch.solc @@ -0,0 +1,248 @@ +import std; +import {callvalue, calldatasize, calldataload, shr} from std.opcodes; + +export { + ABIString, + Contract(*), + ExecMethod, + Fallback(*), + Method(*), + MethodLevelCallvalueCheck, + NonPayable, + Payable, + RunContract, + RunDispatch, + Selector, + SigString, + do_exec, + fallback_default_implementation, + selector_matches, + sigStr +}; + +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +// --- Core Data Types --- + +// A contract contains a tuple of methods and a single fallback +// TODO: implement receive() +enum Contract { Contract(methods, fb) } + +// A method contains an implementation (fn) as well as it's name and type signature +enum Method { Method(Proxy, Proxy, Proxy, Proxy, fn) } + +// Contains the implementation for the fallback (fn) as well as it's type signature +enum Fallback { Fallback(Proxy, Proxy, Proxy, fn) } + +// --- Method Selectors --- + +trait ABIString { // deprecated + function append(head: word, tail: word, prx: Proxy) returns (word) ; +} + +trait SigString { function sigStr(x: Proxy) returns (string) ; } + +function sigStr(p: Proxy) returns (string) where t: SigString { return SigString.sigStr(p); } + +impl SigString { function sigStr(x: Proxy) returns (string) { return "uint256"; }} +impl SigString { function sigStr(x: Proxy) returns (string) { return "bytes32"; }} +impl SigString
{ function sigStr(x: Proxy
) returns (string) { return "address"; }} +impl SigString { function sigStr(x: Proxy) returns (string) { return "string"; }} +impl SigString { function sigStr(x: Proxy) returns (string) { return "bytes"; }} +impl SigString<()> { function sigStr(x: Proxy<()>) returns (string) { return ""; } } + +impl SigString<(a, b)> where a: SigString, b: SigString { + function sigStr(x: Proxy<(a, b)>) returns (string) { + let pa: Proxy = Proxy.Proxy; + let pb: Proxy = Proxy.Proxy; + return SigString.sigStr(pa) + "," + SigString.sigStr(pb); + } +} + +impl SigString> where f: invokable, name: SigString, args: SigString, rets: SigString { + function sigStr(x: Proxy>) returns (string) { + let pname: Proxy = Proxy.Proxy; + let pargs: Proxy = Proxy.Proxy; + return sigStr(pname) + "(" + sigStr(pargs) + ")"; + } +} + + +trait Selector { + function compute(prx: Proxy) returns (bytes4) ; +} + +// Computes the selector hash for a given method +// this is a trait with a single impl since it made some of the downstream definitions a bit cleaner to define +// NOTE: for efficiency purposes this leaves dirty data past the end of the free memory pointer +impl Selector> where name: SigString, args: SigString { + function compute(prx: Proxy>) returns (bytes4) { + // let hash : word = keccakLit(sigStr(prx)); + let pname: Proxy = Proxy.Proxy; + let pargs: Proxy = Proxy.Proxy; + let hash = keccakLit(sigStr(pname) + "(" + sigStr(pargs) + ")"); + return bytes4.bytes4(shr(224, hash)); + } +} + +// --- Method Execution --- + +// Describes how to execute a given method / fallback +trait ExecMethod { + function exec(x: ty) returns () ; +} + +// If fn matches the provided args/ret types, then we can execute any non-payable method +impl ExecMethod> where fn: invokable, args: ABIAttribs, rets: ABIAttribs, ABIDecoder: ABIDecode, rets: ABIEncode { + function exec(m: Method) returns () { + match (m) { case Method.Method(pnm, ppayability, pargs, prets, fn) { // non-payable methods must reject any callvalue before running + MethodLevelCallvalueCheck.checkCallvalue(ppayability); + do_exec(pargs, prets, fn); } } + } +} + +// If fn matches the provided args/ret types, then we can execute any payable method +// payable methods skip the callvalue check entirely +impl ExecMethod> where fn: invokable, args: ABIAttribs, rets: ABIAttribs, ABIDecoder: ABIDecode, rets: ABIEncode { + function exec(m: Method) returns () { + match (m) { case Method.Method(pnm, ppayability, pargs, prets, fn) { do_exec(pargs, prets, fn); } } + } +} + +// Fallbacks have no ABI-decoded inputs or outputs, so the impl is +// specialised to args = rets = () and bypasses the calldata length check +// and ABI decode/encode entirely. +impl ExecMethod> where fn: invokable<(), ()>, payability: MethodLevelCallvalueCheck { + function exec(fb: Fallback) returns () { + match (fb) { case Fallback.Fallback(ppayability, pargs, prets, fn) { MethodLevelCallvalueCheck.checkCallvalue(ppayability); + fn(()); + assembly { + stop() + } } } + } +} + +function do_exec(pargs: Proxy, prets: Proxy, fn: fn) returns () where fn: invokable, args: ABIAttribs, rets: ABIAttribs, ABIDecoder: ABIDecode, rets: ABIEncode { + // check we have enough calldata for the head of args + require(calldatasize() >= (ABIAttribs.headSize(pargs) + 4), Error.Error(0x08638556)); // ABIInputTruncated() + + // TODO: calldatasize checks for dynamic types + + // abi decode args from calldata + let ptr: bytes calldata = calldata.calldata(4); + + // TODO: this needs entirely too many type annotations + let readerProxy: Proxy = Proxy.Proxy; + let args: args = abi_decode(ptr, pargs, readerProxy); + + // call fn with args + // TODO: why are type annotations needed here? + let rets: rets = fn(args); + + // abi encode rets to memory + let ptr = abi_encode(rets); + + // let retSz : word = ABIAttribs.headSize(prets); + // the approach above does not work for dynamically sized types... + // ...instead we take the size of memory allocated by the encoding + let start: word = Typedef.rep(ptr); + let end: word = get_free_memory(); + let retSz: word = end - start; + assembly { + return(start, retSz) + } +} + +// --- Method Dispatch --- + +// For a given tuple of methods this executes the method specified by the first four bytes of calldata +trait RunDispatch { + function go(methods: ty) returns () ; +} + +// We can dispatch to a single executable method with a known selector +impl RunDispatch> where Method: ExecMethod, Method: Selector { + function go(method: Method) returns () { + let methodProxy: Proxy> = Proxy.Proxy; + match (selector_matches(methodProxy)) { case true { ExecMethod.exec(method); } case false { return (); } } + } +} + +// Base case: a contract with no methods has nothing to dispatch to +impl RunDispatch<()> { + function go(methods: ()) returns () { } +} + +// Recursive impl +impl RunDispatch<(n, m)> where n: ExecMethod, n: Selector, m: RunDispatch { + function go(methods: (n, m)) returns () { + let methodProxy: Proxy = Proxy.Proxy; + match (methods) { case (method_n, rest) { match (selector_matches(methodProxy)) { case true { ExecMethod.exec(method_n); } case false { RunDispatch.go(rest); } } } } + } +} + +// TODO: we only wanna do the calldataload once +// Given evidence of a type with a known selector, we can check if it matches the selector in the first four bytes of calldata +function selector_matches(prx: Proxy) returns (bool) where ty: Selector { + let candidate = Typedef.rep(Selector.compute(prx)); + let selector = shr(224, calldataload(0)); + return selector == candidate; +} + +// --- Callvalue Checks --- + +enum Payable {} +enum NonPayable {} + +trait MethodLevelCallvalueCheck { + function checkCallvalue(pty: Proxy) returns () ; +} + +// no callvalue check for Payable methods +impl MethodLevelCallvalueCheck { + function checkCallvalue(prx: Proxy) returns () { } +} +// NonPayable methods revert if passed value +impl MethodLevelCallvalueCheck { + function checkCallvalue(prx: Proxy) returns () { + let NonPayableReceivedValue = Error.Error(0xb5988ea3); + require(callvalue() == 0, NonPayableReceivedValue); + } +} + +// --- Contract Execution --- + +// Describes how to execute a given contract +trait RunContract { + function exec(v: c) returns () ; +} + +// If we have a dispatch for the contracts methods, and we know how to execute it's fallback, then we can define an entrypoint +impl RunContract> where methods: RunDispatch, fb: ExecMethod { + function exec(c: Contract) returns () { + match (c) { case Contract.Contract(ms, fb) { // TODO: if all methods are non payable then we should life the callvalue check here + + // set free memory pointer to the output of memoryguard + // https://docs.soliditylang.org/en/v0.8.30/yul.html#memoryguard + // TODO: we will need to consider immutables here at some point... + assembly { mstore(0x40, memoryguard(128)) } + + // calldata shorter than 4 bytes can't contain a selector — skip + // dispatch and invoke the fallback directly (matches Solidity) + if (calldatasize() >= 4) { + // dispatch to method based on selector + RunDispatch.go(ms); + } + // fallthrough to fallback -- this will be reached upon short input + // or no matching selector + ExecMethod.exec(fb); } } + } +} + +// This is the default fallback used if none is defined. +function fallback_default_implementation() returns () { + let NoSelectorMatchedWithoutFallback = Error.Error(0x4924aef0); + revertWithError(NoSelectorMatchedWithoutFallback); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/std/opcodes.solc b/crates/parser/tests/fixtures/corpus/ok/std/opcodes.solc new file mode 100644 index 00000000..c4ffd066 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/std/opcodes.solc @@ -0,0 +1,693 @@ +// Generated by scripts/gen-std-opcodes.py. Run the script to regenerate. + +export { + stop, + add, + mul, + sub, + div, + sdiv, + mod, + smod, + addmod, + mulmod, + exp, + signextend, + lt, + gt, + slt, + sgt, + eq, + iszero, + and, + or, + xor, + not, + byte, + shl, + shr, + sar, + clz, + keccak256, + address, + balance, + origin, + caller, + callvalue, + calldataload, + calldatasize, + calldatacopy, + codesize, + codecopy, + gasprice, + extcodesize, + extcodecopy, + returndatasize, + returndatacopy, + extcodehash, + blockhash, + coinbase, + timestamp, + number, + prevrandao, + gaslimit, + chainid, + selfbalance, + basefee, + blobhash, + blobbasefee, + pop, + mload, + mstore, + mstore8, + sload, + sstore, + msize, + gas, + tload, + tstore, + mcopy, + log0, + log1, + log2, + log3, + log4, + create, + call, + callcode, + return_, + delegatecall, + create2, + staticcall, + revert_, + invalid, + selfdestruct +}; + +function stop() returns () { + assembly { + stop() + } +} + +function add(a: word, b: word) returns (word) { + let res; + assembly { + res := add(a, b) + } + return res; +} + +function mul(a: word, b: word) returns (word) { + let res; + assembly { + res := mul(a, b) + } + return res; +} + +function sub(a: word, b: word) returns (word) { + let res; + assembly { + res := sub(a, b) + } + return res; +} + +function div(a: word, b: word) returns (word) { + let res; + assembly { + res := div(a, b) + } + return res; +} + +function sdiv(a: word, b: word) returns (word) { + let res; + assembly { + res := sdiv(a, b) + } + return res; +} + +function mod(a: word, b: word) returns (word) { + let res; + assembly { + res := mod(a, b) + } + return res; +} + +function smod(a: word, b: word) returns (word) { + let res; + assembly { + res := smod(a, b) + } + return res; +} + +function addmod(a: word, b: word, c: word) returns (word) { + let res; + assembly { + res := addmod(a, b, c) + } + return res; +} + +function mulmod(a: word, b: word, c: word) returns (word) { + let res; + assembly { + res := mulmod(a, b, c) + } + return res; +} + +function exp(a: word, b: word) returns (word) { + let res; + assembly { + res := exp(a, b) + } + return res; +} + +function signextend(a: word, b: word) returns (word) { + let res; + assembly { + res := signextend(a, b) + } + return res; +} + +function lt(a: word, b: word) returns (word) { + let res; + assembly { + res := lt(a, b) + } + return res; +} + +function gt(a: word, b: word) returns (word) { + let res; + assembly { + res := gt(a, b) + } + return res; +} + +function slt(a: word, b: word) returns (word) { + let res; + assembly { + res := slt(a, b) + } + return res; +} + +function sgt(a: word, b: word) returns (word) { + let res; + assembly { + res := sgt(a, b) + } + return res; +} + +function eq(a: word, b: word) returns (word) { + let res; + assembly { + res := eq(a, b) + } + return res; +} + +function iszero(a: word) returns (word) { + let res; + assembly { + res := iszero(a) + } + return res; +} + +function and(a: word, b: word) returns (word) { + let res; + assembly { + res := and(a, b) + } + return res; +} + +function or(a: word, b: word) returns (word) { + let res; + assembly { + res := or(a, b) + } + return res; +} + +function xor(a: word, b: word) returns (word) { + let res; + assembly { + res := xor(a, b) + } + return res; +} + +function not(a: word) returns (word) { + let res; + assembly { + res := not(a) + } + return res; +} + +function byte(a: word, b: word) returns (word) { + let res; + assembly { + res := byte(a, b) + } + return res; +} + +function shl(a: word, b: word) returns (word) { + let res; + assembly { + res := shl(a, b) + } + return res; +} + +function shr(a: word, b: word) returns (word) { + let res; + assembly { + res := shr(a, b) + } + return res; +} + +function sar(a: word, b: word) returns (word) { + let res; + assembly { + res := sar(a, b) + } + return res; +} + +function clz(a: word) returns (word) { + let res; + assembly { + res := clz(a) + } + return res; +} + +function keccak256(a: word, b: word) returns (word) { + let res; + assembly { + res := keccak256(a, b) + } + return res; +} + +function address() returns (word) { + let res; + assembly { + res := address() + } + return res; +} + +function balance(a: word) returns (word) { + let res; + assembly { + res := balance(a) + } + return res; +} + +function origin() returns (word) { + let res; + assembly { + res := origin() + } + return res; +} + +function caller() returns (word) { + let res; + assembly { + res := caller() + } + return res; +} + +function callvalue() returns (word) { + let res; + assembly { + res := callvalue() + } + return res; +} + +function calldataload(a: word) returns (word) { + let res; + assembly { + res := calldataload(a) + } + return res; +} + +function calldatasize() returns (word) { + let res; + assembly { + res := calldatasize() + } + return res; +} + +function calldatacopy(a: word, b: word, c: word) returns () { + assembly { + calldatacopy(a, b, c) + } +} + +function codesize() returns (word) { + let res; + assembly { + res := codesize() + } + return res; +} + +function codecopy(a: word, b: word, c: word) returns () { + assembly { + codecopy(a, b, c) + } +} + +function gasprice() returns (word) { + let res; + assembly { + res := gasprice() + } + return res; +} + +function extcodesize(a: word) returns (word) { + let res; + assembly { + res := extcodesize(a) + } + return res; +} + +function extcodecopy(a: word, b: word, c: word, d: word) returns () { + assembly { + extcodecopy(a, b, c, d) + } +} + +function returndatasize() returns (word) { + let res; + assembly { + res := returndatasize() + } + return res; +} + +function returndatacopy(a: word, b: word, c: word) returns () { + assembly { + returndatacopy(a, b, c) + } +} + +function extcodehash(a: word) returns (word) { + let res; + assembly { + res := extcodehash(a) + } + return res; +} + +function blockhash(a: word) returns (word) { + let res; + assembly { + res := blockhash(a) + } + return res; +} + +function coinbase() returns (word) { + let res; + assembly { + res := coinbase() + } + return res; +} + +function timestamp() returns (word) { + let res; + assembly { + res := timestamp() + } + return res; +} + +function number() returns (word) { + let res; + assembly { + res := number() + } + return res; +} + +function prevrandao() returns (word) { + let res; + assembly { + res := prevrandao() + } + return res; +} + +function gaslimit() returns (word) { + let res; + assembly { + res := gaslimit() + } + return res; +} + +function chainid() returns (word) { + let res; + assembly { + res := chainid() + } + return res; +} + +function selfbalance() returns (word) { + let res; + assembly { + res := selfbalance() + } + return res; +} + +function basefee() returns (word) { + let res; + assembly { + res := basefee() + } + return res; +} + +function blobhash(a: word) returns (word) { + let res; + assembly { + res := blobhash(a) + } + return res; +} + +function blobbasefee() returns (word) { + let res; + assembly { + res := blobbasefee() + } + return res; +} + +function pop(a: word) returns () { + assembly { + pop(a) + } +} + +function mload(a: word) returns (word) { + let res; + assembly { + res := mload(a) + } + return res; +} + +function mstore(a: word, b: word) returns () { + assembly { + mstore(a, b) + } +} + +function mstore8(a: word, b: word) returns () { + assembly { + mstore8(a, b) + } +} + +function sload(a: word) returns (word) { + let res; + assembly { + res := sload(a) + } + return res; +} + +function sstore(a: word, b: word) returns () { + assembly { + sstore(a, b) + } +} + +function msize() returns (word) { + let res; + assembly { + res := msize() + } + return res; +} + +function gas() returns (word) { + let res; + assembly { + res := gas() + } + return res; +} + +function tload(a: word) returns (word) { + let res; + assembly { + res := tload(a) + } + return res; +} + +function tstore(a: word, b: word) returns () { + assembly { + tstore(a, b) + } +} + +function mcopy(a: word, b: word, c: word) returns () { + assembly { + mcopy(a, b, c) + } +} + +function log0(a: word, b: word) returns () { + assembly { + log0(a, b) + } +} + +function log1(a: word, b: word, c: word) returns () { + assembly { + log1(a, b, c) + } +} + +function log2(a: word, b: word, c: word, d: word) returns () { + assembly { + log2(a, b, c, d) + } +} + +function log3(a: word, b: word, c: word, d: word, e: word) returns () { + assembly { + log3(a, b, c, d, e) + } +} + +function log4(a: word, b: word, c: word, d: word, e: word, f: word) returns () { + assembly { + log4(a, b, c, d, e, f) + } +} + +function create(a: word, b: word, c: word) returns (word) { + let res; + assembly { + res := create(a, b, c) + } + return res; +} + +function call(a: word, b: word, c: word, d: word, e: word, f: word, g: word) returns (word) { + let res; + assembly { + res := call(a, b, c, d, e, f, g) + } + return res; +} + +function callcode(a: word, b: word, c: word, d: word, e: word, f: word, g: word) returns (word) { + let res; + assembly { + res := callcode(a, b, c, d, e, f, g) + } + return res; +} + +function return_(a: word, b: word) returns () { + assembly { + return(a, b) + } +} + +function delegatecall(a: word, b: word, c: word, d: word, e: word, f: word) returns (word) { + let res; + assembly { + res := delegatecall(a, b, c, d, e, f) + } + return res; +} + +function create2(a: word, b: word, c: word, d: word) returns (word) { + let res; + assembly { + res := create2(a, b, c, d) + } + return res; +} + +function staticcall(a: word, b: word, c: word, d: word, e: word, f: word) returns (word) { + let res; + assembly { + res := staticcall(a, b, c, d, e, f) + } + return res; +} + +function revert_(a: word, b: word) returns () { + assembly { + revert(a, b) + } +} + +function invalid() returns () { + assembly { + invalid() + } +} + +function selfdestruct(a: word) returns () { + assembly { + selfdestruct(a) + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/std/std.solc b/crates/parser/tests/fixtures/corpus/ok/std/std.solc new file mode 100644 index 00000000..44f6e627 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/std/std.solc @@ -0,0 +1,2081 @@ +import {add, sub, mul, div, mod, exp as exp_, addmod as addmod_, mulmod as mulmod_, and as and_, or as or_, xor as xor_, shl, shr, eq, not as not_, gt as gt_, iszero, keccak256, mstore, mload, mcopy, sstore, sload, gas, calldataload, calldatacopy, returndatasize, returndatacopy, log1 as log1_, call, staticcall, revert_, invalid} from std.opcodes; + +pragma solcore noPattersonCondition ABIEncode, Num; +pragma solcore noCoverageCondition ABIDecode, MemoryType; + +export { + ABIAttribs, + ABIDecode, + ABIDecoder(*), + ABIEncode, + ABITuple(*), + Add, + Assign, + BitAnd, + BitOr, + BitXor, + Bounded, + CalldataWordReader(*), + CanStore, + ContractStorage(*), + Div, + DynArray, + Error(*), + Eq, + HasWordReader, + IndexAccess, + LVA, + LValueIdxAccess, + MemberAccessProxy(*), + MemoryEncode, + MemoryPointer, + MemorySize, + MemoryType, + MemoryWordReader(*), + Mod, + Mul, + Num, + Ord, + Pow, + Proxy(*), + RVA, + RValueIdxAccess, + StorageSize, + StorageType, + StructField(*), + Sub, + Typedef, + WordReader, + abi_decode, + abi_encode, + addWord, + addmod, + allocateDynamicArray, + address(*), + allocate_memory, + allocate_zeroed_memory, + and, + assert, + byte(*), + bytes, + bytes4(*), + bytes32(*), + bandWord, + borWord, + bxorWord, + bnotWord, + bshlWord, + bshrWord, + calldata(*), + concat, + concatLit, + ecrecover, + empty(*), + eqWord, + erc7201, + frombool, + ge, + getReader, + get_free_memory, + gt, + gtWord, + hash1, + hash2, + keccak256_, + keccakLit, + le, + lidx, + loadBytesFromStorage, + log1, + lt, + mapping(*), + maxVal, + memberAccessBase, + memory(*), + memory_ref, + mulmod, + ne, + not, + or, + out_of_bounds, + raw_call, + readStorage, + returndata(*), + revertLit, + revertEmpty, + revertWithError, + require, + ridx, + ripemd160, + round_up_to_mul_of_32, + rval, + set_free_memory, + sha256, + slice(*), + slice_, + storage(*), + storeBytesFromMemory, + string, + strlen, + strlenLit, + subWord, + truncate, + toWord, + to_bytes, + tobool, + uint256(*), + unimplemented, + zeroize_memory +}; + +/* +- features + - primitive word eq + - include stdlib + - MPTC + optional weak args (MPTC formalization?) + - surface for loops + - better inference for Typedef.rep() calls (have to annotate atm?) + - boolean short circuiting +- sugar + - Proxy (e.g. `@t ==> Proxy : Proxy t` + - IndexAccess reads (e.g. `x[i] ==> IndexAccess.get(x, i)`) + - auto typedef instances +- syntax + - order of type args + - braces for blocks in matches + - trait / impl syntax + - function -> fn? + - assembly vs high level return? +- todo + - abi decoding + - contract desugaring + - mappings + - strings + - full range of uintX / intX / bytesX types + - address types + - statically sized arrays + - tuple field access + - structs + - define numeric tower + - fixed point types + - fixed point numeric routines + - memory vectors +*/ + + +function log1(v: t, topic: word) returns () where t: Typedef { + let w: word = Typedef.rep(v); + mstore(0, w); + log1_(0, 32, topic); +} + +function unimplemented() returns () { + let Unimplemented = Error.Error(0x6e128399); + revertWithError(Unimplemented); +} + +function out_of_bounds() returns () { + let OutOfBounds = Error.Error(0xb4120f14); + revertWithError(OutOfBounds); +} + +// ------------------------------------------------------------------ +// High-level revert helper +// ------------------------------------------------------------------ +// EmitHull has special handling for `revertLit("...")` after MastEval has +// constant-folded the argument to a string literal. +function revertLit(s: string) returns () { + unimplemented(); // Sanity check if folding ignores it. + return (); +} + +// Empty revert. +function revertEmpty() returns () { + revert_(0, 0); +} + +// TODO: use bytes4 +// TODO: add literal version Msg(string) +enum Error { Error(word), Empty, Msg(string memory) } + +// Revert with Error selector. +function revertWithError(e: Error) returns () { + match (e) { case Error.Error(selector) { mstore(0, selector); + // We only care about the BE MSB. + revert_(28, 4); } case Error.Empty { revert_(0, 0); } case Error.Msg(msg) { let msg_ = Typedef.rep(msg); + revert_(msg_ + 32, mload(msg_)); } } +} + +function assert(cond: bool) returns () { + if (!cond) { + invalid(); + } +} + +function require(cond: bool, e: Error) returns () { + if (!cond) { + revertWithError(e); + } +} + +// --- booleans --- + +// TODO: this should short circuit. probably needs some compiler magic to do so. +function and(x: bool, y: bool) returns (bool) { + match (x, y) { case (true, y) { return y; } case (false, _) { return false; } } +} + +// TODO: this should short circuit. probably needs some compiler magic to do so. +function or(x: bool, y: bool) returns (bool) { + match (x, y) { case (true, _) { return true; } case (false, y) { return y; } } +} + +function not(b: bool) returns (bool) { + match (b) { case false { return true; } case true { return false; } } +} + +function frombool(b: bool) returns (word) { + match (b) { case false { return 0; } case true { return 1; } } +} + +function tobool(x: word) returns (bool) { + match (x) { case 0 { return false; } default { return true; } } +} + +// --- Tuple projections --- + +function fst(p: (a, b)) returns (a) { + match (p) { case (a, _) { return a; } } +} + +function snd(p: (a, b)) returns (b) { + match (p) { case (_, b) { return b; } } +} + +// --- Proxy --- + +// Proxy is a unit type that can be used to pass Types as paramaters at runtime +enum Proxy { Proxy } + +// --- Type Abstraction --- + +trait Typedef { + function abs(x: rep) returns (abs) ; + function rep(x: abs) returns (rep) ; +} + +default impl Typedef { + function abs(x: t) returns (t) { return x; } + function rep(x: t) returns (t) { return x; } +} + +// --- Equality --- +// Note: All these are used by the compiler by name. + +trait Eq { + function eq(x: a, y: a) returns (bool) ; +} + +function ne(x: a, y: a) returns (bool) where a: Eq { + return not(Eq.eq(x,y)); +} + +// --- Ordering --- +// Note: All these are used by the compiler by name. + +trait Ord where a: Eq { + function gt(x: a, y: a) returns (bool) ; +} + +function gt(x: a, y: a) returns (bool) where a: Ord { + return Ord.gt(x,y); +} + +function le(x: a, y: a) returns (bool) where a: Ord { + return not(Ord.gt(x,y)); +} + +function ge(x: a, y: a) returns (bool) where a: Ord { + return le(y,x); +} + +function lt(x: a, y: a) returns (bool) where a: Ord { + return Ord.gt(y,x); +} + +// --- Arithmetic --- +// Note: All these are used by the compiler by name. + +trait Add { + function add(l: t, r: t) returns (t) ; +} + +trait Sub { + function sub(l: t, r: t) returns (t) ; +} + +trait Mul { + function mul(l: t, r: t) returns (t) ; +} + +trait Pow { + function pow(l: t, r: t) returns (t) ; +} + +trait Div { + function div(l: t, r: t) returns (t) ; +} + +trait Mod { + function mod(l: t, r: t) returns (t) ; +} + +trait BitAnd { + function band(l: t, r: t) returns (t) ; +} + +trait BitOr { + function bor(l: t, r: t) returns (t) ; +} + +trait BitXor { + function bxor(l: t, r: t) returns (t) ; +} + +trait Bounded { + function minVal() returns (t) ; + function maxVal() returns (t) ; +} + +function maxVal() returns (t) where t: Bounded { return Bounded.maxVal(); } + +// umbrella trait +trait Num where a: Add, a: Sub, a: Bounded, a: Eq, a: Ord, a: Typedef { + function maxVal() returns (a) ; + function toWord(x: a) returns (word) ; + function fromWord(x: word) returns (a) ; + function fromInteger(comptime x: integer) returns (comptime a) ; + function add(x: a, y: a) returns (a) ; + function sub(x: a, y: a) returns (a) ; + function gt(x: a, y: a) returns (bool) ; +} + +default impl Num where a: Add, a: Sub, a: Bounded, a: Eq, a: Ord, a: Typedef { + function maxVal() returns (a) { return Bounded.maxVal(); } + function toWord(x: a) returns (word) { return Typedef.rep(x); } + function fromWord(x: word) returns (a) { return Typedef.abs(x); } + function fromInteger(comptime x: integer) returns (comptime a) { return Typedef.abs(wordFromInteger(x)); } + function add(x: a, y: a) returns (a) { return Add.add(x,y); } + function sub(x: a, y: a) returns (a) { return Sub.sub(x,y); } + function gt(x: a, y: a) returns (bool) { return Ord.gt(x, y); } +} + +// --- Word Arithmetic & Logic --- +// TODO: make these checked + +// These are intended to be folded by MastEval when their arguments are +// statically known word values. +function eqWord(x: word, y: word) returns (bool) { + return tobool(eq(x, y)); +} + +function gtWord(x: word, y: word) returns (bool) { + return tobool(gt_(x, y)); +} + +function addWord(l: word, r: word) returns (word) { + return add(l, r); +} + +function subWord(l: word, r: word) returns (word) { + return sub(l, r); +} + +// Bitwise AND +function bandWord(x: word, y: word) returns (word) { + return and_(x, y); +} + +// Bitwise OR +function borWord(x: word, y: word) returns (word) { + return or_(x, y); +} + +// Bitwise XOR +function bxorWord(x: word, y: word) returns (word) { + return xor_(x, y); +} + +// Bitwise NOT +function bnotWord(x: word) returns (word) { + return not_(x); +} + +// Bitwise SHL +function bshlWord(x: word, y: word) returns (word) { + return shl(x, y); +} + +// Bitwise SHR +function bshrWord(x: word, y: word) returns (word) { + return shr(x, y); +} + +impl Eq { + function eq(x: word, y: word) returns (bool) { + return eqWord(x, y); + } +} + +impl Ord { + function gt(x: word, y: word) returns (bool) { + return gtWord(x, y); + } +} + +impl Add { + function add(l: word, r: word) returns (word) { + return addWord(l, r); + } +} + +impl Sub { + function sub(l: word, r: word) returns (word) { + return subWord(l, r); + } +} + +function mulWord(l: word, r: word) returns (word) { + return mul(l, r); +} + +impl Mul { + function mul(l: word, r: word) returns (word) { + return mulWord(l, r); + } +} + +impl Pow { + function pow(l: word, r: word) returns (word) { + return exp_(l, r); + } +} + +impl Div { + function div(l: word, r: word) returns (word) { + return div(l, r); + } +} + +impl Mod { + function mod(l: word, r: word) returns (word) { + return mod(l, r); + } +} + +impl BitAnd { + function band(l: word, r: word) returns (word) { + return bandWord(l, r); + } +} + +impl BitOr { + function bor(l: word, r: word) returns (word) { + return borWord(l, r); + } +} + +impl BitXor { + function bxor(l: word, r: word) returns (word) { + return bxorWord(l, r); + } +} + +impl Eq { + function eq(x: integer, y: integer) returns (bool) { + return integerEq(x, y); + } +} + +impl Ord { + function gt(x: integer, y: integer) returns (bool) { + return integerLt(y, x); + } +} + +impl Add { + function add(l: integer, r: integer) returns (integer) { + return integerAdd(l, r); + } +} + +impl Sub { + function sub(l: integer, r: integer) returns (integer) { + return integerSub(l, r); + } +} + +impl Mul { + function mul(l: integer, r: integer) returns (integer) { + return integerMul(l, r); + } +} + +impl Bounded { + function maxVal() returns (word) { + return 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + } + function minVal() returns (word) { + return 0; + } +} + +function hash1(x: word) returns (word) { + mstore(0, x); + return keccak256(0, 32); +} + +function hash2(x: word, y: word) returns (word) { + mstore(0, x); + mstore(32, y); + return keccak256(0, 64); +} + +// --- Value Types --- + +function toWord(x: t) returns (word) where t: Typedef { return Typedef.rep(x); } + +enum uint256 { uint256(word) } +impl Typedef { + function abs(w: word) returns (uint256) { + return uint256.uint256(w); + } + + function rep(x: uint256) returns (word) { + match (x) { case uint256.uint256(w) { return w; } } + } +} +impl Add { + function add(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Add.add(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Sub { + function sub(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Sub.sub(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Mul { + function mul(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Mul.mul(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Pow { + function pow(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Pow.pow(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Div { + function div(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Div.div(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Mod { + function mod(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(Mod.mod(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl BitAnd { + function band(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(BitAnd.band(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl BitOr { + function bor(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(BitOr.bor(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl BitXor { + function bxor(x: uint256, y: uint256) returns (uint256) { + return Typedef.abs(BitXor.bxor(Typedef.rep(x), Typedef.rep(y))); + } +} + +impl Eq { + function eq(x: uint256, y: uint256) returns (bool) { + return Eq.eq(Typedef.rep(x), Typedef.rep(y)); + } +} + +impl Ord { + function gt(x: uint256, y: uint256) returns (bool) { + return Ord.gt(Typedef.rep(x), Typedef.rep(y)); + } +} + +impl Bounded { + function maxVal() returns (uint256) { + return uint256.uint256(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + } + function minVal() returns (uint256) { + return uint256.uint256(0); + } +} + +impl Int { + function fromInteger(x: integer) returns (uint256) { + return uint256.uint256(wordFromInteger(x)); + } +} + +function addmod(x: uint256, y: uint256, k: uint256) returns (uint256) { + require(k != uint256.uint256(0), Error.Error(0x7125cbb9)); // AddModWithZero() + return Typedef.abs(addmod_(Typedef.rep(x), Typedef.rep(y), Typedef.rep(k))); +} + +function mulmod(x: uint256, y: uint256, k: uint256) returns (uint256) { + require(k != uint256.uint256(0), Error.Error(0xdaea23b9)); // MulModWithZero() + return Typedef.abs(mulmod_(Typedef.rep(x), Typedef.rep(y), Typedef.rep(k))); +} + +enum byte { byte(word) } +impl Typedef { + function abs(w: word) returns (byte) { + return byte.byte(w); + } + + function rep(x: byte) returns (word) { + match (x) { case byte.byte(w) { return w; } } + } +} + +// --- Address --- +enum address { address(word) } + +impl Typedef { + function rep(x: address) returns (word) { + match (x) { case address.address(y) { return y; } } + } + function abs(x: word) returns (address) { + return address.address(x); + } +} + +impl Eq
{ + function eq(x: address, y: address) returns (bool) { + return Eq.eq(Typedef.rep(x), Typedef.rep(y)); + } +} + +// --- Bytes4 --- + +enum bytes4 { bytes4(word) } + +impl Typedef { + function rep(b: bytes4) returns (word) { + match (b) { case bytes4.bytes4(w) { return w; } } + } + function abs(w: word) returns (bytes4) { + return bytes4.bytes4(w); + } +} + +// --- Bytes32 --- + +enum bytes32 { bytes32(word) } + +impl Typedef { + function rep(b: bytes32) returns (word) { + match (b) { case bytes32.bytes32(w) { return w; } } + } + function abs(w: word) returns (bytes32) { + return bytes32.bytes32(w); + } +} + +impl Eq { + function eq(x: bytes32, y: bytes32) returns (bool) { + return Eq.eq(Typedef.rep(x), Typedef.rep(y)); + } +} + +impl Ord { + function gt(x: bytes32, y: bytes32) returns (bool) { + return Ord.gt(Typedef.rep(x), Typedef.rep(y)); + } +} + +// --- Pointers --- + +enum memory { memory(word) } +impl Typedef { + function abs(x: word) returns (t memory) { + return memory.memory(x); + } + + function rep(x: t memory) returns (word) { + match (x) { case memory.memory(w) { return w; } } + } +} + +enum storage { storage(word) } +impl Typedef { + function abs(x: word) returns (t storage) { + return storage.storage(x); + } + + function rep(x: t storage) returns (word) { + match (x) { case storage.storage(w) { return w; } } + } +} + +enum calldata { calldata(word) } +impl Typedef { + function abs(x: word) returns (t calldata) { + return calldata.calldata(x); + } + + function rep(x: t calldata) returns (word) { + match (x) { case calldata.calldata(w) { return w; } } + } +} + +enum returndata { returndata(word) } +impl Typedef, word> { + function abs(x: word) returns (returndata) { + return returndata.returndata(x); + } + + function rep(x: returndata) returns (word) { + match (x) { case returndata.returndata(w) { return w; } } + } +} + +enum mapping { mapping(word) } + +// --- Low-level memory ops + +function strlen(s: string memory) returns (word) { + match (s) { case memory.memory(a) { return mload(a); } } +} + +// --- Memory Utilities --- + +// Memory in solidity is bump allocated in a single arena +// The word stored in memory at index 0x40 is used to store the start of the currently unused memory region + +// returns the value stored in memory(0x40) +function get_free_memory() returns (word) { + return mload(0x40); +} + +// set the value stored in memory(0x40) +function set_free_memory(loc: word) returns () { + mstore(0x40, loc); +} + +// Allocate memory and update the memory pointer. +function allocate_memory(size: word) returns (word) { + let ptr = get_free_memory(); + set_free_memory(ptr + size); + return ptr; +} + +function allocate_zeroed_memory(size: word) returns (word) { + let ptr = allocate_memory(size); + zeroize_memory(ptr, size); + return ptr; +} + +// Clears a memory area. +function zeroize_memory(ptr: word, len: word) returns () { + let end_ptr = ptr + len; + + // Zero out 32-byte words. + for (let i = 0; i < len / 32; i += 1) { + mstore(ptr, 0); + ptr += 32; + } + + // Zero out trailing bytes. We rely on the zero-slot (0x60-0x7f). + mcopy(ptr, 0x60, end_ptr - ptr); +} + +// --- Indexable Types --- + +// types that can be written to and read from at a uint256 index +// TODO: this needs to be split into LValue / RValue variants for `=` desugaring +trait IndexAccess { + function get(c: t, i: uint256) returns (val) ; + function set(c: t, i: uint256, v: val) returns () ; +} + +// --- DynArray --- + +// Word arrays with a size known only at runtime +// types with a size smaller than `word` will not be packed, so a `DynArray(byte)` will waste a lot of space +// TODO: storage representation +enum DynArray {} + +impl IndexAccess memory, t> where t: Typedef { + function get(ptr: DynArray memory, i: uint256) returns (t) { + let i_: word = Typedef.rep(i); + let loc = Typedef.rep(ptr); + let res: word; + match (i_ > mload(loc)) { case false { res = mload((i_ * 32) + loc); } case true { out_of_bounds(); } } + return Typedef.abs(res); + } + function set(arr: DynArray memory, i: uint256, val: t) returns () { + let i_: word = Typedef.rep(i); + let loc: word = Typedef.rep(arr); + match (i_ > mload(loc)) { case false { mstore((i_ * 32) + loc, Typedef.rep(val)); } case true { out_of_bounds(); } } + } +} + +function allocateDynamicArray(prx: Proxy, length: word) returns (DynArray memory) { + // size of allocation in bytes + let sz: word = (length + 1) * 32; + + // get start of array & increment free by sz + let free: word = get_free_memory(); + set_free_memory(free + sz); + + // write array length and return + mstore(free, length); + let res: DynArray memory = Typedef.abs(free); + return res; +} + +// --- bytes --- + +// tightly packed byte arrays +// bytes does not have a runtime representation since it can only ever exist in +// memory / calldata / storage and serves only as a type tag for pointer types +// TODO: IndexAccess for memory(bytes) +// TODO: IndexAccess for calldata(bytes) +// TODO: IndexAccess for storage(bytes) +enum bytes {} + +// --- strings --- + +// TODO: should this be a typedef over `bytes`? +enum string {} + +impl Add { + function add(l: string, r: string) returns (string) { + return concatLit(l, r); + } +} + +// ------------------------------------------------------------------ +// Compile-time string literal builtins +// ------------------------------------------------------------------ +// These are intended to be folded by MastEval when their arguments are +// statically known string literals. + +function concatLit(a: string, b: string) returns (string) { + unimplemented(); // Sanity check if folding ignores it. + return ""; +} + +function strlenLit(a: string) returns (word) { + unimplemented(); // Sanity check if folding ignores it. + return 0; +} + +function keccakLit(a: string) returns (word) { + unimplemented(); // Sanity check if folding ignores it. + return 0; +} + +// --- slices (sized pointers) --- + +// A slice is a wrapper around an existing pointer type that extends the +// underlying type with information about the size of the data pointed to by `t` +enum slice { slice(ptr, word) } + +// --- Word Reader --- + +// A WordReader is an abstraction over byte indexed structure that can be read in word sized chunks (e.g. calldata / memory) +// These let us use the same abi decoding routines for calldata / memory +trait WordReader { + // returns the word currently pointed to by the WordReader + function read(reader: ty) returns (word) ; + // returns a new WordReader that points to a location `offset` bytes further into the array + function advance(reader: ty, offset: word) returns (ty) ; + // copies a block from the underlying source to memory + function copyToMem(reader: ty, dst: word, cnt: word) returns () ; +} + +// WordReader for memory +enum MemoryWordReader { MemoryWordReader(word) } +impl WordReader { + function read(reader: MemoryWordReader) returns (word) { + match (reader) { case MemoryWordReader.MemoryWordReader(ptr) { return mload(ptr); } } + } + function advance(reader: MemoryWordReader, offset: word) returns (MemoryWordReader) { + match (reader) { case MemoryWordReader.MemoryWordReader(ptr) { return MemoryWordReader.MemoryWordReader(ptr + offset); } } + } + function copyToMem(reader: MemoryWordReader, dst: word, cnt: word) returns () { + match (reader) { case MemoryWordReader.MemoryWordReader(ptr) { mcopy(dst, ptr, cnt); } } + } +} + +// WordReader for calldata +enum CalldataWordReader { CalldataWordReader(word) } + +impl Typedef { + function abs(a: word) returns (CalldataWordReader) { return CalldataWordReader.CalldataWordReader(a); } + function rep(r: CalldataWordReader) returns (word) { + match (r) { case CalldataWordReader.CalldataWordReader(a) { return a; } } + } +} + +impl WordReader { + function read(reader: CalldataWordReader) returns (word) { + match (reader) { case CalldataWordReader.CalldataWordReader(ptr) { return calldataload(ptr); } } + } + function advance(reader: CalldataWordReader, offset: word) returns (CalldataWordReader) { + match (reader) { case CalldataWordReader.CalldataWordReader(ptr) { return CalldataWordReader.CalldataWordReader(ptr + offset); } } + } + function copyToMem(reader: CalldataWordReader, dst: word, cnt: word) returns () { + match (reader) { case CalldataWordReader.CalldataWordReader(ptr) { calldatacopy(dst, ptr, cnt); } } + } +} + +// --- HasWordReader --- + +// The HasWordReader trait defines the types for which a WordReader can be produced +// We define impls for memory(bytes) and calldata(bytes) +trait HasWordReader { + function getWordReader(x: self) returns (reader) ; +} + +impl HasWordReader { + function getWordReader(x: bytes memory) returns (MemoryWordReader) { + return MemoryWordReader.MemoryWordReader(Typedef.rep(x)); + } +} + +impl HasWordReader { + function getWordReader(x: bytes calldata) returns (CalldataWordReader) { + return CalldataWordReader.CalldataWordReader(Typedef.rep(x)); + } +} + +// --- MemoryType --- + +// The MemoryType trait abstracts over type specific logic related to memory +// layout, allowing us to write code that is generic over which type is held in memory +trait MemoryType { + // Proxy needed because trait methods must mention strong type params + // loads a `loadedType` value from a `self` value located at `loc` in memory + function loadFromMemory(p: Proxy, loc: word) returns (loadedType) ; +} + +// A uint256 can be loaded from memory and pushed straight onto the stack +impl MemoryType { + function loadFromMemory(p: Proxy, loc: word) returns (uint256) { + return uint256.uint256(mload(loc)); + } +} + +// We load a DynArray into a sized pointer to the first element +/* +forall ty ret . ty:MemoryType(ret) => instance DynArray(ty):MemoryType(slice(memory(ret))) { + function loadFromMemory(p : Proxy (DynArray(ty)), loc:word) -> slice(memory(ret)) { + let length = mload(loc); + return slice(Typedef.abs(loc) : memory(ret), length); + } +} +*/ + +// FAIL: patterson +// FAIL: bound variable +// if we ty is a MemoryType that returns deref and deref is ABIEncode, then we can encode a memory(ty) +// by loading it and then running the ABI encoding for the loaded value +/* +forall ty deref . ty:MemoryType(deref), deref:ABIEncode => instance memory(ty):ABIEncode { + function encodeInto(x:memory(ty), basePtr:word, offset:word, tail:word) -> word { + let prx : Proxy(ty); // FIXED: before was Proxy(deref) + return ABIEncode.encodeInto(MemoryType.loadFromMemory(prx, Typedef.rep(x)) : deref, basePtr, offset, tail); + } +} +*/ +// --- ABI Tuples --- + +// Tuples in Solidity are always desugared to nested pairs (to allow for +// inductive trait impl constructions) . +// This is an issue for the ABI routines since the ABI spec differentiates +// between `(1,1,1)` and `(1,(1,1))`, but the language treats both identically. +// The ABITuple type lets us reiintroduce this distinction: +// `ABITuple((1,(1,1))` should be treated as `(1,1,1)` for the purposes of ABI +// encoding / decoding. +enum ABITuple { ABITuple(tuple) } + +impl Typedef, t> { + function abs(t: t) returns (ABITuple) { + return ABITuple.ABITuple(t); + } + + function rep(x: ABITuple) returns (t) { + match (x) { case ABITuple.ABITuple(v) { return v; } } + } +} + +// --- ABI Metadata --- + +// Statically knowable ABI related metadata about `self` +trait ABIAttribs { + // how many bytes should be used for the head portion of the abi encoding of `self` + function headSize(ty: Proxy) returns (word) ; + // whether or not `self` is a fully static type + function isStatic(ty: Proxy) returns (bool) ; +} + +default impl ABIAttribs { + function headSize(ty: Proxy) returns (word) { return 32; } + function isStatic(ty: Proxy) returns (bool) { return true; } +} + +impl ABIAttribs<()> { + function headSize(ty: Proxy<()>) returns (word) { return 0; } + function isStatic(ty: Proxy<()>) returns (bool) { return true; } +} +impl ABIAttribs { + function headSize(ty: Proxy) returns (word) { return 32; } + function isStatic(ty: Proxy) returns (bool) { return true; } +} +impl ABIAttribs
{ + function headSize(ty: Proxy
) returns (word) { return 32; } + function isStatic(ty: Proxy
) returns (bool) { return true; } +} +impl ABIAttribs> { + function headSize(ty: Proxy>) returns (word) { return 32; } + function isStatic(ty: Proxy>) returns (bool) { return false; } +} +impl ABIAttribs { + function headSize(ty: Proxy) returns (word) { return 32; } + function isStatic(ty: Proxy) returns (bool) { return false; } +} + +// computes the attribs for a pair of two types that implement attribs +impl ABIAttribs<(a, b)> where a: ABIAttribs, b: ABIAttribs { + function headSize(ty: Proxy<(a, b)>) returns (word) { + let pa: Proxy; + let pb: Proxy; + let sza = ABIAttribs.headSize(pa); + let szb = ABIAttribs.headSize(pb); + return sza + szb; + } + function isStatic(ty: Proxy<(a, b)>) returns (bool) { + let pa: Proxy; + let pb: Proxy; + return and(ABIAttribs.isStatic(pa), ABIAttribs.isStatic(pb)); + } +} + +// if an abi tuple contains dynamic elems we store it in the tail, otherwise we +// treat it the same as a series of nested pairs +impl ABIAttribs> where tuple: ABIAttribs { + function headSize(ty: Proxy>) returns (word) { + let px: Proxy; + match (ABIAttribs.isStatic(px)) { case true { return ABIAttribs.headSize(px); } case false { return 32; } } + } + function isStatic(ty: Proxy>) returns (bool) { + let px: Proxy; + return ABIAttribs.isStatic(px); + } +} + +// for pointer types we fetch the attribs of the pointed to type, not the pointer itself +impl ABIAttribs where ty: ABIAttribs { + function headSize(p: Proxy) returns (word) { + let px: Proxy; + return ABIAttribs.headSize(px); + } + function isStatic(p: Proxy) returns (bool) { + let px: Proxy; + return ABIAttribs.isStatic(px); + } +} +impl ABIAttribs where ty: ABIAttribs { + function headSize(p: Proxy) returns (word) { + let px: Proxy; + return ABIAttribs.headSize(px); + } + function isStatic(ty: Proxy) returns (bool) { + let px: Proxy; + return ABIAttribs.isStatic(px); + } +} + +// --- ABI Encoding --- +// TODO: make these generic over the location being written to (i.e. memory or returndata) + +// top level encoding function. +// abi encodes a `ty` value and returns a pointer to the result +function abi_encode(val: ty) returns (bytes memory) where ty: ABIAttribs, ty: ABIEncode { + let free = get_free_memory(); + let valueProxy: Proxy = Proxy.Proxy; + let tail = ABIEncode.encodeInto(val, free, 0, free + ABIAttribs.headSize(valueProxy)); + set_free_memory(tail); + return memory.memory(free); +} + +// types that can be abi encoded +trait ABIEncode { + // abi encodes a self value into a memory region starting at basePtr + // offset gives the offset in memory from basePtr to the first empty byte of the head + // tail gives the index in memory of the first empty byte of the tail + /* newTail */ +function encodeInto(x: self, basePtr: word, offset: word, tail: word) returns (word) ; +} + +impl ABIEncode { + // a unit256 is written directly into the head + function encodeInto(x: uint256, basePtr: word, offset: word, tail: word) returns (word) { + let repx: word = Typedef.rep(x); + mstore(basePtr + offset, repx); + return tail; + } +} + +impl ABIEncode
{ + // an address is written directly into the head (into a full 32-byte slot) + function encodeInto(x: address, basePtr: word, offset: word, tail: word) returns (word) { + let repx: word = Typedef.rep(x); + mstore(basePtr + offset, repx); + return tail; + } +} + +impl ABIEncode { + // a bytes32 is written directly into the head + function encodeInto(x: bytes32, basePtr: word, offset: word, tail: word) returns (word) { + let repx: word = Typedef.rep(x); + mstore(basePtr + offset, repx); + return tail; + } +} + +impl ABIEncode { + function encodeInto(x: bool, basePtr: word, offset: word, tail: word) returns (word) { + let repx: word = frombool(x); + mstore(basePtr + offset, repx); + return tail; + } +} + +function round_up_to_mul_of_32(value: word) returns (word) { + return and_(value + 31, not_(31)); +} + +function encodeIntoFromBytesLike(srcPtr: word, basePtr: word, offset: word, tail: word) returns (word) { + let length = mload(srcPtr); + let total = length + 32; + mstore(basePtr + offset, tail - basePtr); + mcopy(tail, srcPtr, total); + let rounded = round_up_to_mul_of_32(total); + zeroize_memory(tail + total, rounded - total); + return tail + rounded; +} + +impl ABIEncode { + function encodeInto(x: string memory, basePtr: word, offset: word, tail: word) returns (word) { + return encodeIntoFromBytesLike(Typedef.rep(x), basePtr, offset, tail); + } +} + +impl ABIEncode { + function encodeInto(x: bytes memory, basePtr: word, offset: word, tail: word) returns (word) { + return encodeIntoFromBytesLike(Typedef.rep(x), basePtr, offset, tail); + } +} + +impl ABIEncode<()> { + // a unit256 is written directly into the head + function encodeInto(x: (), basePtr: word, offset: word, tail: word) returns (word) { + return tail; + } +} + +// abi encoding for a pair of two encodable types +impl ABIEncode<(a, b)> where a: ABIAttribs, a: ABIEncode, b: ABIEncode { + function encodeInto(x: (a, b), basePtr: word, offset: word, tail: word) returns (word) { + match (x) { case (l, r) { let newTail = ABIEncode.encodeInto(l, basePtr, offset, tail); + let pa: Proxy; + let a_sz = ABIAttribs.headSize(pa); + return ABIEncode.encodeInto(r, basePtr, offset + a_sz, newTail); } } + } +} + + +// abi encoding for an ABITuple of encodable types +// TODO: is this correct? +impl ABIEncode> where tuple: ABIEncode, tuple: ABIAttribs { + function encodeInto(x: ABITuple, basePtr: word, offset: word, tail: word) returns (word) { + let prx: Proxy; + match (ABIAttribs.isStatic(prx)) { + case true { + return ABIEncode.encodeInto(Typedef.rep(x), basePtr, offset, tail); + } + case false { + // if the tuple contains dynamically sized elements then we store a + // pointer in the head, and encode the tuple into the tail + // store the length of the head in basePtr + mstore(basePtr, tail - basePtr); + + // encode the underlying tuple into the tail + let tupleProxy: Proxy = Proxy.Proxy; + let headSize = ABIAttribs.headSize(tupleProxy); + basePtr = tail; + tail = tail + headSize; + return ABIEncode.encodeInto(Typedef.rep(x), basePtr, 0, tail); + } + } + } +} + +// --- ABI Decoding --- + +// Top level decoding function. +// abi decodes a `decodable` value into a `ty` +function abi_decode(decodable: decodable, pty: Proxy, prdr: Proxy) returns (decoded) where decodable: HasWordReader, ABIDecoder: ABIDecode { + let decoder: ABIDecoder = ABIDecoder.ABIDecoder(HasWordReader.getWordReader(decodable)); + return ABIDecode.decode(decoder, 0); +} + + +trait ABIDecode { + function decode(ptr: decoder, currentHeadOffset: word) returns (decoded) ; +} + +// An ABI Decoder for `ty` from `reader` +// This lets us abstract over memory and calldata when decoding +enum ABIDecoder { ABIDecoder(reader) } + +// If `reader` is a `WordReader` then so is our `ABIDecoder` +impl WordReader> where reader: WordReader { + function read(decoder: ABIDecoder) returns (word) { + match (decoder) { case ABIDecoder.ABIDecoder(ptr) { return WordReader.read(ptr); } } + } + function advance(decoder: ABIDecoder, offset: word) returns (ABIDecoder) { + match (decoder) { case ABIDecoder.ABIDecoder(ptr) { return ABIDecoder.ABIDecoder(WordReader.advance(ptr, offset)); } } + } + function copyToMem(decoder: ABIDecoder, dst: word, cnt: word) returns () { + match (decoder) { case ABIDecoder.ABIDecoder(ptr) { WordReader.copyToMem(ptr, dst, cnt); } } + } +} + +// ABI Decoding for uint256 +impl ABIDecode, uint256> where reader: WordReader { + function decode(ptr: ABIDecoder, currentHeadOffset: word) returns (uint256) { + return Typedef.abs(WordReader.read(WordReader.advance(ptr, currentHeadOffset))); + } +} + +// ABI Decoding for bytes32 +impl ABIDecode, bytes32> where reader: WordReader { + function decode(ptr: ABIDecoder, currentHeadOffset: word) returns (bytes32) { + return Typedef.abs(WordReader.read(WordReader.advance(ptr, currentHeadOffset))); + } +} + +// ABI Decoding for address +impl ABIDecode, address> where reader: WordReader { + function decode(ptr: ABIDecoder, currentHeadOffset: word) returns (address) { + let raw = WordReader.read(WordReader.advance(ptr, currentHeadOffset)); + require(shr(160, raw) == 0, Error.Error(0x7cc04fa7)); // DirtyHigherBitsForAddress() + return Typedef.abs(raw); + } +} + +impl ABIDecode, ()> where reader: WordReader { + function decode(ptr: ABIDecoder<(), reader>, currentHeadOffset: word) returns () { + return (); + } +} + +// ABI decoding for bytes/strings (only in memory) +function decodeBytesLike(ptr: ABIDecoder, currentHeadOffset: word) returns (a memory) where reader: WordReader { + let tmp: word; + let headRdr = WordReader.advance(ptr, currentHeadOffset); + let tailPtr: word = WordReader.read(headRdr); + + let src = WordReader.advance(ptr, tailPtr); + let srcRdr = getReader(src); + let length = WordReader.read(src); + let total = length + 32; + let rounded = round_up_to_mul_of_32(total); + let resultPtr: word = allocate_memory(rounded); + WordReader.copyToMem(srcRdr, resultPtr, total); + return memory.memory(resultPtr); +} + +// ABI decoding for strings (only in memory) +impl ABIDecode, string memory> where reader: WordReader { + function decode(ptr: ABIDecoder, currentHeadOffset: word) returns (string memory) { + return decodeBytesLike(ptr, currentHeadOffset); + } +} + +// ABI decoding for bytes (only in memory) +impl ABIDecode, bytes memory> where reader: WordReader { + function decode(ptr: ABIDecoder, currentHeadOffset: word) returns (bytes memory) { + return decodeBytesLike(ptr, currentHeadOffset); + } +} + +// ABI decoding for a pair of decodable values +// FAIL: Coverage +impl ABIDecode, (a_decoded, b_decoded)> where reader: WordReader, ABIDecoder: ABIDecode, ABIDecoder: ABIDecode, a: ABIAttribs { + function decode(ptr: ABIDecoder<(a, b), reader>, currentHeadOffset: word) returns (a_decoded, b_decoded) { + match (ptr) { case ABIDecoder.ABIDecoder(rdr) { let prx: Proxy; + let decoder_a: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + let decoder_b: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + let a_val: a_decoded = ABIDecode.decode(decoder_a, currentHeadOffset); + let b_val: b_decoded = ABIDecode.decode(decoder_b, currentHeadOffset + ABIAttribs.headSize(prx)); + return (a_val, b_val); } } + } +} + +impl ABIDecode, reader>, tuple_decoded> where reader: WordReader, tuple: ABIDecode, tuple: ABIAttribs { + function decode(ptr: ABIDecoder, reader>, currentHeadOffset: word) returns (tuple_decoded) { + let prx: Proxy; + match (ABIAttribs.isStatic(prx)) { case true { return ABIDecode.decode(WordReader.advance(ptr, currentHeadOffset), 0); } case false { let tailPtr = WordReader.read(ptr); + return ABIDecode.decode(WordReader.advance(ptr, tailPtr), 0); } } + } +} + + +impl ABIDecode memory, reader>, tuple_decoded memory> where reader: WordReader, tuple: ABIDecode, tuple: ABIAttribs { + function decode(ptr: ABIDecoder memory, reader>, currentHeadOffset: word) returns (tuple_decoded memory) { + let prx: Proxy; + match (ABIAttribs.isStatic(prx)) { case true { return ABIDecode.decode(WordReader.advance(ptr, currentHeadOffset), 0); } case false { let tailPtr = WordReader.read(ptr); + return ABIDecode.decode(WordReader.advance(ptr, tailPtr), 0); } } + } +} + +impl ABIDecode memory, reader>, DynArray memory> where baseType: ABIAttribs, reader: WordReader, ABIDecoder: ABIDecode { + function decode(ptr: ABIDecoder memory, reader>, currentHeadOffset: word) returns (DynArray memory) { + let arrayPtr = WordReader.advance(ptr, currentHeadOffset); + let length = WordReader.read(arrayPtr); + // this trigger a missing typedef constraint + // let elementPtr:ABIDecoder(baseType, reader) = Typedef.abs(WordReader.advance(arrayPtr, 32)); + arrayPtr = WordReader.advance(arrayPtr, 32); + let prx: Proxy; + let result: DynArray memory = allocateDynamicArray(prx, length); + let offset: word = 0; + let prx: Proxy; + let elementHeadSize: word = ABIAttribs.headSize(prx); + + // TODO: surface level loops + // TODO: sugar for assigning to indexAccess types (result[i]) + //for(let i = 0; i < length; i++) { + //result[i] = ABIDecode.decode(elementPtr, offset); + //assembly { offset := add(offset, elementHeadSize) } + //} + + return result; + } +} + +function getReader(d: ABIDecoder) returns (reader) { + match (d) { case ABIDecoder.ABIDecoder(rdr) { return rdr; } } +} + +impl ABIDecode calldata, CalldataWordReader>, DynArray calldata> where ABIDecoder: ABIDecode, baseType: WordReader { + function decode(ptr: ABIDecoder calldata, CalldataWordReader>, currentHeadOffset: word) returns (DynArray calldata) { + let newptr = WordReader.advance(ptr, currentHeadOffset); + let reader: CalldataWordReader = getReader(newptr); + let addr: word = Typedef.rep(reader); + return Typedef.abs(addr); + } + } + + +// --- Assignment --- + +/* +# Types and traits for assignment desugaring +- access proxy types +- LValue and RValue access traits (LVA, RVA) +- Assign trait +*/ + + +pragma solcore noPattersonCondition RVA, Assign; +pragma solcore noCoverageCondition MemberAccessProxy, LVA, RVA, CStructField, Assign; +pragma solcore noBoundVariableCondition LVA, RVA; +// -- storage + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + + +default impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} +/* +instance uint:StorageSize { + function size(x:Proxy(uint)) -> word { + return 1; + } +} +*/ +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize
{ + function size(x: Proxy
) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + return a_sz + b_sz; + } +} + +trait StorageType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +impl StorageType { + function load(ptr: word) returns (word) { + return sload(ptr); + } + function store(ptr: word, value: word) returns () { + sstore(ptr, value); + } +} + +impl StorageType { + function load(ptr: word) returns (uint256) { return uint256.uint256(StorageType.load(ptr)); } + function store(ptr: word, value: uint256) returns () { StorageType.store(ptr, Typedef.rep(value)); } +} + +impl StorageType { + function load(ptr: word) returns (bytes32) { return bytes32.bytes32(StorageType.load(ptr)); } + function store(ptr: word, value: bytes32) returns () { StorageType.store(ptr, Typedef.rep(value)); } +} + +impl StorageType
{ + function load(ptr: word) returns (address) { return address.address(StorageType.load(ptr)); } + function store(ptr: word, value: address) returns () { StorageType.store(ptr, Typedef.rep(value)); } +} + +// -- structure fields (including contract fields) + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessBase(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + +impl LVA, fieldSelector, loadType, offsetType>, storageType storage> where StructField, fieldSelector>: CStructField, offsetType: StorageSize, storageType storage: CanStore { + function acc(x: MemberAccessProxy, fieldSelector, loadType, offsetType>) returns (storageType storage) { + let offsetProxy: Proxy = Proxy.Proxy; + let offset: word = StorageSize.size(offsetProxy); + let result: storageType storage = storage.storage(offset); + return result; + } +} + +impl RVA, fieldSelector, loadType, offsetType>, loadType> where StructField, fieldSelector>: CStructField, storageType storage: CanStore, offsetType: StorageSize { + function acc(x: MemberAccessProxy, fieldSelector, loadType, offsetType>) returns (loadType) { + let offsetProxy: Proxy = Proxy.Proxy; + let offset: word = StorageSize.size(offsetProxy); + let storageRef: storageType storage = storage.storage(offset); + let result: loadType = CanStore.load(storageRef); + return result; + } +} + +// TODO: structures other than contract context +/* +forall structType fieldSelector fieldType storageType offsetType + . StructField(structType, fieldSelector):CStructField(fieldType, offsetType) + , offsetType:StorageSize + => instance MemberAccessProxy(storage(structType), fieldSelector, fieldType, offsetType):LVA(storage(fieldType)) { + function acc(x:MemberAccessProxy(storage(structType), fieldSelector, fieldType, offsetType)) -> storage(fieldType) { + let ptr:word = Typedef.rep(memberAccessBase(x)); + let size:word = StorageSize.size(Proxy:Proxy(offsetType)); + return storage(ptr + size); + } +} + +forall structType fieldSelector fieldType storageType offsetType + . StructField(structType, fieldSelector):CStructField(fieldType, offsetType) + , offsetType:StorageSize + , fieldType:StorageType + => instance MemberAccessProxy(storage(structType), fieldSelector, fieldType, offsetType):RVA(fieldType) { + function acc(x:MemberAccessProxy(storage(structType), fieldSelector, fieldType, offsetType)) -> fieldType { + let ptr:word = Typedef.rep(memberAccessBase(x)); + let size:word = StorageSize.size(Proxy:Proxy(offsetType)); + return CanStore.load(ptr + size); + } +} +*/ + + + +enum ContractStorage { ContractStorage(cxt) } + + +impl Typedef member), word> { + function rep(x: mapping(index => member)) returns (word) { + match (x) { case mapping.mapping(y) { return y; } } + } + function abs(x: word) returns (mapping(index => member)) { + return mapping.mapping(x); + } +} + + +// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays +impl StorageSize member)> { + function size(x: Proxy member)>) returns (word) { + return 1; + } +} + +trait LVA { + function acc(x: self) returns (memberRefType) ; +} + + +trait RVA { + function acc(x: self) returns (member) ; +} + +function rval(x: a) returns (b) where a: RVA { + return RVA.acc(x); +} + + +// TODO: consider merging CanStore and Assign +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + + +// a can store b; e.g. storage(string) : memory(string) +trait CanStore { + function store(r: a, v: b) returns () ; + function load(r: a) returns (b) ; +} + + +impl Assign where a: CanStore { + function assign(l: a, r: b) returns () { + CanStore.store(l, r); + } +} + +/* +forall a. a:StorageType => +default instance a:CanStore(a) { + function store(l:storage(a), r:a) -> () { + StorageType.store(Typedef.rep(l), r); + } + function load(l:storage(a)) -> a { + return StorageType.load(Typedef.rep(l)); + } +} +*/ + + impl CanStore { + function store(l: word storage, r: word) returns () { + StorageType.store(Typedef.rep(l), r); + } + function load(l: word storage) returns (word) { + return StorageType.load(Typedef.rep(l)); + } +} + + impl CanStore { + function store(l: uint256 storage, r: uint256) returns () { + StorageType.store(Typedef.rep(l), r); + } + function load(l: uint256 storage) returns (uint256) { + return StorageType.load(Typedef.rep(l)); + } +} + + impl CanStore { + function store(l: bytes32 storage, r: bytes32) returns () { + StorageType.store(Typedef.rep(l), r); + } + function load(l: bytes32 storage) returns (bytes32) { + return StorageType.load(Typedef.rep(l)); + } +} + + impl CanStore
{ + function store(l: address storage, r: address) returns () { + StorageType.store(Typedef.rep(l), r); + } + function load(l: address storage) returns (address) { + return StorageType.load(Typedef.rep(l)); + } +} + +impl CanStore v) storage, mapping(k => v) storage> { + function store(l: mapping(k => v) storage, r: mapping(k => v) storage) returns () { + // StorageType.store(Typedef.rep(l), r); + unimplemented(); + } + function load(l: mapping(k => v) storage) returns (mapping(k => v) storage) { + // return StorageType.load(Typedef.rep(l)); + unimplemented(); + return l; + } +} + + +impl CanStore { + function store(dst: string storage, src: string memory) returns () { + let srcPtr: word = Typedef.rep(src); + let slot = Typedef.rep(dst); + storeBytesFromMemory(slot, srcPtr); + } + + function load(src: string storage) returns (string memory) { + let srcPtr: word = Typedef.rep(src); + let dstPtr: word = get_free_memory(); + let endPtr = loadBytesFromStorage(srcPtr, dstPtr); + set_free_memory(endPtr); + return memory.memory(dstPtr); + } +} + +// bytes share the same storage layout as string, so the same +// storeBytesFromMemory / loadBytesFromStorage helpers apply. +impl CanStore { + function store(dst: bytes storage, src: bytes memory) returns () { + let srcPtr: word = Typedef.rep(src); + let slot = Typedef.rep(dst); + storeBytesFromMemory(slot, srcPtr); + } + + function load(src: bytes storage) returns (bytes memory) { + let srcPtr: word = Typedef.rep(src); + let dstPtr: word = get_free_memory(); + let endPtr = loadBytesFromStorage(srcPtr, dstPtr); + set_free_memory(endPtr); + return memory.memory(dstPtr); + } +} + +// Shamelessly stolen from function copy_byte_array_to_storage_from_t_bytes_memory_ptr_to_t_bytes_storage +// TODO: consider wrapping behaviour at end of storage +function storeBytesFromMemory(slot: word, src: word) returns () { + assembly { + let newLen := mload(src) + // TODO: check old len, cleanup etc + let srcOffset := 32 + switch gt(newLen, 31) + case 1 { + mstore(0,slot) + let dstPtr := keccak256(0,32) + let loopEnd := and(newLen, not(0x1f)) + let i := 0 + for { } lt(i, loopEnd) { i := add(i, 0x20) } { + sstore(dstPtr, mload(add(src, srcOffset))) + dstPtr := add(dstPtr, 1) + srcOffset := add(srcOffset, 32) + } + if lt(loopEnd, newLen) { + let lastValue := mload(add(src, srcOffset)) + let lastLen := and(newLen, 0x1f) + let mask := not(shr(mul(8, lastLen), not(0))) + let nudata := and(lastValue, mask) // a Yul variable cannot be called "data". Go figure. + sstore(dstPtr, nudata) + } + sstore(slot, add(mul(newLen, 2), 1)) + } + default { + let value := 0 + if newLen { + value := mload(add(src, srcOffset)) + } + let mask := not(shr(mul(8, newLen), not(0))) + let nudata := and(value, mask) + let used := or(nudata, mul(2, newLen)) + sstore(slot,used) + } + } +} + + +// shamelessly stolen from abi_encode_t_string_storage_to_t_string_memory_ptr +function loadBytesFromStorage(slot: word, memPtr: word) returns (word) { + let pos = memPtr; + let slotValue = sload(slot); + let length = slotValue / 2; + let outOfPlaceEncoding = tobool(and_(slotValue, 1)); + if (!outOfPlaceEncoding) { + length = and_(length, 0x7f); + } + mstore(pos, length); + pos += 32; + match (outOfPlaceEncoding) { case false { // Short byte array + mstore(pos, and_(slotValue, not_(0xff))); + let empty = iszero(length); + let notzero = iszero(empty); + return pos + (notzero * 32); } case true { // Long byte array + let dataPos = hash1(slot); + let i = 0; + for (; i < length; i += 32) { + mstore(pos + i, sload(dataPos)); + dataPos += 1; + } + return pos + i; } } +} + + +// -- Tuple-based indexed access: + +trait RValueIdxAccess { + function lookup(ci: col_idx) returns (val) ; +} + +trait LValueIdxAccess { + function lookup(ci: col_idx) returns (ref) ; +} + +impl LValueIdxAccess<(mapping(i => a) storage, i), a storage> where i: Typedef { + function lookup(xi: (mapping(i => a) storage, i)) returns (a storage) { + match (xi) { case (x, i) { return storage.storage(hash2(Typedef.rep(x), Typedef.rep(i))); } } + } +} + +impl RValueIdxAccess<(mapping(i => a) storage, i), a> where a: StorageType, i: Typedef { + function lookup(xi: (mapping(i => a) storage, i)) returns (a) { + /* + match(xi) { + | (x, i) => return StorageType.load(hash2(Typedef.rep(x), Typedef.rep(i))); + } + */ + return readStorage(LValueIdxAccess.lookup(xi)); + } +} + +function readStorage(x: a storage) returns (a) where a: StorageType { + return StorageType.load(Typedef.rep(x)); +} +/* +forall r a. a:StorageType, r: RValueIdxAccess(a) => +function rval(x:r) -> a { + return RValueIdxAccess.lookup(x); +} + +forall r a. r: LValueIdxAccess(a) => +function lval(x:r) -> a { + return LValueIdxAccess.lookup(x); +} +*/ + +function lidx(m: mapping(i => a) storage, x: i) returns (a storage) where i: Typedef { + return storage.storage(hash2(Typedef.rep(m), Typedef.rep(x))); +} + +function ridx(m: mapping(i => a) storage, x: i) returns (a) where i: Typedef, a: StorageType { + return StorageType.load(hash2(Typedef.rep(m), Typedef.rep(x))); +} + +// --- Memory Encoding --- + +trait MemorySize { + // The size needed for the value. + function len(v: t) returns (word) ; +} + +// NOTE: this is not implemented for value types. +trait MemoryPointer { + // In-memory location of the given value. + function ptr(v: t) returns (word) ; +} + +trait MemoryEncode { + // Serialize the entire contents at a provided memory area. + function encodeInto(v: t, target: word) returns () ; +} + +// TODO: support variadic arguments +// Allocates new memory and concatenates the inputs into it. +function concat(x: a, y: b) returns (bytes memory) where a: MemorySize, a: MemoryEncode, b: MemorySize, b: MemoryEncode { + let x_len = MemorySize.len(x); + let y_len = MemorySize.len(y); + let res: word = allocate_memory(32 + x_len + y_len); + mstore(res, x_len + y_len); + MemoryEncode.encodeInto(x, res + 32); + MemoryEncode.encodeInto(y, res + 32 + x_len); + return memory.memory(res); +} + +// This is a specialized 1-input version of concat. +function to_bytes(x: a) returns (bytes memory) where a: MemorySize, a: MemoryEncode { + let len = MemorySize.len(x); + let res = allocate_memory(32 + len); + mstore(res, len); + MemoryEncode.encodeInto(x, res + 32); + return memory.memory(res); +} + +impl MemorySize { + function len(v: bytes32) returns (word) { + return 32; + } +} + +impl MemoryEncode { + function encodeInto(v: bytes32, target: word) returns () { + mstore(target, Typedef.rep(v)); + } +} + +impl MemorySize { + function len(v: bytes memory) returns (word) { + return mload(Typedef.rep(v)); + } +} + +impl MemoryPointer { + function ptr(v: bytes memory) returns (word) { + return Typedef.rep(v) + 32; + } +} + +impl MemoryEncode { + function encodeInto(v: bytes memory, target: word) returns () { + let v_ = Typedef.rep(v); + mcopy(target, v_ + 32, mload(v_)); + } +} + +// Placeholder for an empty memory area. +// The value is the size of the area in bytes. The area will be zeroed upon serialization. +// NOTE: not implementing Typedef by design. +enum empty { empty(word) } + +impl MemorySize { + function len(v: empty) returns (word) { + match (v) { case empty.empty(size) { return size; } } + } +} + +impl MemoryEncode { + function encodeInto(v: empty, target: word) returns () { + let size; + match (v) { case empty.empty(size_) { size = size_; } } + zeroize_memory(target, size); + } +} + +// --- Memory Slices --- + +// This is a very cheap abstraction over a memory area of [ptr, ptr+len) +// No type information is preserved. +enum memory_ref { memory_ref(word, word) } + +impl MemorySize { + function len(v: memory_ref) returns (word) { + match (v) { case memory_ref.memory_ref(ptr, len) { return len; } } + } +} + +impl MemoryPointer { + function ptr(v: memory_ref) returns (word) { + match (v) { case memory_ref.memory_ref(ptr, len) { return ptr; } } + } +} + +impl MemoryEncode { + function encodeInto(v: memory_ref, target: word) returns () { + match (v) { case memory_ref.memory_ref(ptr, len) { mcopy(target, ptr, len); } } + } +} + +function slice_(input: a, start: word) returns (memory_ref) where a: MemorySize, a: MemoryPointer { + let len = MemorySize.len(input); + // TODO: should this allow (it does now) a zero-length slice? + require(len >= start, Error.Error(0xb4120f14)); // OutOfBounds() + let ptr_ = MemoryPointer.ptr(input); + return memory_ref.memory_ref(ptr_ + start, len - start); +} + +function truncate(input: a, end: word) returns (memory_ref) where a: MemorySize, a: MemoryPointer { + let len = MemorySize.len(input); + // TODO: should this allow (it does now) a zero-length slice? + require(len >= end, Error.Error(0xb4120f14)); // OutOfBounds() + return memory_ref.memory_ref(MemoryPointer.ptr(input), end); +} + +// --- Hashing --- + +// NOTE: keccak256 name conflicts with assembly namespace +function keccak256_(input: a) returns (bytes32) where a: MemorySize, a: MemoryPointer { + let len: word = MemorySize.len(input); + let ptr: word = MemoryPointer.ptr(input); + return bytes32.bytes32(keccak256(ptr, len)); +} + +function sha256(input: a) returns (bytes32) where a: MemorySize, a: MemoryPointer { + let len: word = MemorySize.len(input); + let ptr: word = MemoryPointer.ptr(input); + // We assume the [0, 32] scratch space is reserved. + let ret = staticcall(gas(), 2, ptr, len, 0, 32); + require(ret != 0, Error.Error(0x68c071bb)); // SHA256CallFailed() + return bytes32.bytes32(mload(0)); +} + +function ripemd160(input: a) returns (bytes32) where a: MemorySize, a: MemoryPointer { + let len: word = MemorySize.len(input); + let ptr: word = MemoryPointer.ptr(input); + // We assume the [0, 32] scratch space is reserved. + let ret = staticcall(gas(), 3, ptr, len, 0, 32); + require(ret != 0, Error.Error(0x31a72d92)); // RIPEMD160CallFailed() + return bytes32.bytes32(mload(0)); +} + +// --- Precompiles --- + +// Perform an ECDSA signature recovery. It ensures the call has succeeded, +// and that the signature is not malleable (s ≤ secp256k1n/2). Transactions +// were updated to ban this, but the precompile wasn't. If a user relies on that +// feature they can call the precompile via assembly. +// TODO: use uint8 +function ecrecover(hash: bytes32, v: uint256, r: bytes32, s: bytes32) returns (address) { + // MalleableSignatureRejected() + require( + Typedef.rep(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + Error.Error(0x25260b20) + ); + + let hash_ = Typedef.rep(hash); + let v_ = Typedef.rep(v); + let r_ = Typedef.rep(r); + let s_ = Typedef.rep(s); + let ptr = get_free_memory(); + // We assume the [0, 32] scratch space is reserved. + mstore(ptr, hash_); + mstore(ptr + 32, v_); + mstore(ptr + 64, r_); + mstore(ptr + 96, s_); + let ret = staticcall(gas(), 1, ptr, 128, 0, 32); + require(ret != 0, Error.Error(0x578763f7)); // ECRecoverCallFailed() + let res = mload(0); + require(res != 0, Error.Error(0x4fbfae63)); // ECRecoverFailed() + return address.address(res); +} + +// TODO: use string here +// TODO: eventually this needs to become comptime +function erc7201(id: bytes memory) returns (bytes32) { +// return keccak256_(to_bytes(keccak256_(id) - 1)) & ~0xff; + return Typedef.abs( + and_( + Typedef.rep( + keccak256_( + to_bytes(bytes32.bytes32(Typedef.rep(keccak256_(id)) - 1)) + ) + ), + not_(0xff) + ) + ); +} + +function raw_call(target: address, value: uint256, payload: a) returns (bool, bytes memory) where a: MemorySize, a: MemoryPointer { + let ret = call( + gas(), + Typedef.rep(target), + Typedef.rep(value), + MemoryPointer.ptr(payload), + MemorySize.len(payload), + 0, + 0 + ); + let retSize = returndatasize(); + let retData = allocate_memory(32 + retSize); + mstore(retData, retSize); + // TODO: use returndatacopy(retData + 32, 0, retSize);, but it is a parser error + // See https://github.com/argotorg/solcore/issues/497 + assembly { + returndatacopy(add(retData, 32), 0, retSize) + } + return (tobool(ret), memory.memory(retData)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Ackermann.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Ackermann.solc new file mode 100644 index 00000000..390b54a4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Ackermann.solc @@ -0,0 +1,6 @@ +enum Nat { Zero, Succ(Nat) } + +function foo(x: Nat, y: Nat) returns (word) { + match (y, x) { case (y1, Nat.Zero) { return 1 ; } case (Nat.Zero, Nat.Succ(x2)) { return 2; } case (Nat.Succ(y3), Nat.Succ(x3)) { return 3; } } +} + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Add1.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Add1.solc new file mode 100644 index 00000000..72baa53a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Add1.solc @@ -0,0 +1,9 @@ +contract Add1 { + function main() public returns (word) { + let res: word; + assembly { + res := add(40, 2) + } + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/BoolNot.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/BoolNot.solc new file mode 100644 index 00000000..09948466 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/BoolNot.solc @@ -0,0 +1,5 @@ +enum Bool { False, True } + +function not(b: Bool) returns (Bool) { + match (b) { case Bool.False { return Bool.True ; } case Bool.True { return Bool.False ; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose.solc new file mode 100644 index 00000000..4d465a89 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose.solc @@ -0,0 +1,7 @@ +contract Compose { + function id(x: word) public returns (word) { return x; } + + function main() public returns (word) { + return id(id(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose3.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose3.solc new file mode 100644 index 00000000..4da95232 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Compose3.solc @@ -0,0 +1,11 @@ +contract Compose { + function id(x: a) public returns (a) { return x; } + + function apply1(f: function(word) returns (word), a: word) public returns (word) { return f(a); } + + function idThenId(x: word) public returns (word) { return id(id(x)); } + + function main() public returns (word) { + return apply1(idThenId, 42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/CondExp.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/CondExp.solc new file mode 100644 index 00000000..7b70d19e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/CondExp.solc @@ -0,0 +1,6 @@ +contract CondExp { + function main() public returns (word) { + return + ((true ? false : true) ? (false ? 1 : 2) : (true ? 42 : 56)); + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/DuplicateFun.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/DuplicateFun.solc new file mode 100644 index 00000000..e6e9202b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/DuplicateFun.solc @@ -0,0 +1,21 @@ + +trait A { + function foo(p: self) returns (word) ; +} + +trait B { + function foo(p: self) returns (word) ; +} + +impl B { + function foo(x: word) returns (word) { + return x; + } +} + +// error: Constraint for A not found in type of foo +impl A { + function foo(x: word) returns (word) { + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EitherModule.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EitherModule.solc new file mode 100644 index 00000000..99107cc0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EitherModule.solc @@ -0,0 +1,10 @@ +contract EitherModule { + enum Either { Left(a), Right(b) } + enum List { Nil, Cons(a, List) } + + function lefts(xs: List>) public returns (List) { + match (xs) { case List.Nil { return List.Nil ; } case List.Cons(y, ys) { match (y) { case Either.Left(z) { return List.Cons(z,lefts(ys)) ; } case Either.Right(z) { return lefts(ys) ; } } } } + } + + function main() public returns (word) { return 0; } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EqQual.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EqQual.solc new file mode 100644 index 00000000..750442e5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EqQual.solc @@ -0,0 +1,19 @@ +enum Bool { True, False } + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +trait Ord where a: Eq { + function lt(x: a, y: a) returns (Bool) ; +} + +impl Eq { + function eq(x: word, y: word) returns (Bool) { + match (primEqWord(x,y)) { case 0 { return Bool.False; } default { return Bool.True ; } } + } +} + +function foo(x: word) returns (Bool) { + return Eq.eq (x, 0); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EvenOdd.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EvenOdd.solc new file mode 100644 index 00000000..4f1af8eb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/EvenOdd.solc @@ -0,0 +1,14 @@ +contract EvenOdd { + enum Nat { Zero, Succ(Nat) } + enum Bool { False, True } + + function even(n: Nat) public returns (Bool) { + match (n) { case Nat.Zero { return Bool.True; } case Nat.Succ(m) { return odd(m); } } + } + + function odd(n: Nat) public returns (Bool) { + match (n) { case Nat.Zero { return Bool.False; } case Nat.Succ(m) { return even(m); } } + } + + function main() public returns (word) { return 0; } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Foo.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Foo.solc new file mode 100644 index 00000000..5adbcda0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Foo.solc @@ -0,0 +1,9 @@ + function one() returns (word) { + return primAddWord(1, zero()) ; + } + + function zero() returns (word) { + return 0; + } + + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Id.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Id.solc new file mode 100644 index 00000000..d00cde77 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Id.solc @@ -0,0 +1,10 @@ +function id(x: word) returns (word) { + return x; +} + +contract Id { + function main() public returns (word) { + return id(0); + } +} + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ListModule.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ListModule.solc new file mode 100644 index 00000000..b5f175d8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ListModule.solc @@ -0,0 +1,17 @@ +contract ListModule { + enum List { Nil, Cons(a, List) } + enum Bool { True, False } + + + function zipWith(f: function(a, b) returns (c), xs: List, ys: List) public returns (List) { + match (xs, ys) { case (List.Nil, List.Nil) { return List.Nil ; } case (List.Cons(x1, xs1), List.Cons(y1, ys1)) { return List.Cons(f(x1,y1), zipWith(f,xs1,ys1)) ; } default { return List.Nil; } } + } + + function foldr(f: function(a, b) returns (b), v: b, xs: List) public returns (b) { + match (xs) { case List.Nil { return v; } case List.Cons(y, ys) { return f(y, foldr(f,v,ys)) ; } } + } + + function main() public returns (word) { + return 0; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Logic.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Logic.solc new file mode 100644 index 00000000..5f956556 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Logic.solc @@ -0,0 +1,21 @@ +contract Logic { + enum Bool { True, False } + + function not(x: Bool) public returns (Bool) { + match (x) { case Bool.True { return Bool.False ; } case Bool.False { return Bool.True ; } } + } + + function and(x: Bool, y: Bool) public returns (Bool) { + match (x, y) { case (Bool.False, _) { return Bool.False ; } case (Bool.True, _) { return y ; } } + } + + function and1(x: Bool, y: Bool) public returns (Bool) { + match (x, y) { case (Bool.False, Bool.False) { return Bool.False ; } case (Bool.True, Bool.False) { return Bool.False; } case (Bool.False, Bool.True) { return Bool.False; } case (Bool.True, Bool.True) { return Bool.True; } } + } + + function elim(f: word, g: word, x: Bool) public returns (word) { + match (x) { case Bool.True { return f; } case Bool.False { return g; } } + } + + function main() public returns (word) { return 0; } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/MatchCall.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/MatchCall.solc new file mode 100644 index 00000000..11c8a951 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/MatchCall.solc @@ -0,0 +1,11 @@ +enum Bool { False, True } + +contract MatchCall { + function f() public returns (Bool) { + return Bool.True; + } + + function main() public returns (word) { + match (f()) { case Bool.True { return 42; } case Bool.False { return 0; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory1.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory1.solc new file mode 100644 index 00000000..8f52767d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory1.solc @@ -0,0 +1,7 @@ +enum memory { memory(word) } + +function g() returns () { + let x: word memory; + let y: word memory = memory.memory(1); + x = memory.memory(0); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory2.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory2.solc new file mode 100644 index 00000000..ae79dd03 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Memory2.solc @@ -0,0 +1,5 @@ +enum Memory { Memory(word) } + +function g() returns () { + let x: Memory> = Memory.Memory(0); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Mutuals.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Mutuals.solc new file mode 100644 index 00000000..09ac8593 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Mutuals.solc @@ -0,0 +1,8 @@ +contract Mutual { + function main() public returns (word) { + return f(); + } + function f() public returns (word) { + return 42; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/NegPair.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/NegPair.solc new file mode 100644 index 00000000..45766493 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/NegPair.solc @@ -0,0 +1,40 @@ + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + +function fst(p: (a, b)) returns (a) { + match (p) { case (x, y) { return x; } } +} + +function snd(p: (a, b)) returns (b) { + match (p) { case (x, y) { return y; } } +} + + +impl Neg<(a, b)> where a: Neg, b: Neg { + function neg(p: (a, b)) returns (a, b) { + return (Neg.neg (fst(p)), Neg.neg(snd (p))); + } +} + +contract NegPair { + + function bnot(x: B) public returns (B) { + match (x) { case B.T { return B.F; } case B.F { return B.T; } } +} + + function fromB(b: B) public returns (word) { + match (b) { case B.F { return 0; } case B.T { return 1; } } +} + + function main() public returns (word) { return fromB(fst(Neg.neg((B.F,B.T)))); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Option.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Option.solc new file mode 100644 index 00000000..da564195 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Option.solc @@ -0,0 +1,9 @@ +contract Option { + enum Option { None, Some(a) } + + function join(mmx: Option>) public returns (Option) { + match (mmx) { case Option.None { return Option.None; } case Option.Some(Option.Some(x)) { return Option.Some(x); } case Option.Some(Option.None) { return Option.None; } } + } + + function main() public returns (word) { return 0; } + } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Pair.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Pair.solc new file mode 100644 index 00000000..7dfc81a3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Pair.solc @@ -0,0 +1,19 @@ + function fst(x: (a, b)) returns (a) { + match (x) { case (a, _) { return a; } } + } + + function snd(x: (a, b)) returns (b) { + match (x) { case (_, b) { return b; } } + } + + function uncurry(f: function(word, word) returns (word), x: (word, word)) returns (word) { + match (x) { case (a, b) { return f(a,b); } } + } + + function snds(p1: (word, word), p2: (word, word)) returns (word, word) { + match (p1, p2) { case ((a, b), (c, d)) { return (b,d); } } + } + + function curry(f: function((word, word)) returns (word), x: word, y: word) returns (word) { + return f((x,y)) ; + } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Peano.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Peano.solc new file mode 100644 index 00000000..ca06f46d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Peano.solc @@ -0,0 +1,9 @@ +enum Nat { Zero, Succ(Nat) } + +function natInd(step: function(Nat, Nat) returns (Nat), v: Nat, n: Nat) returns (Nat) { + match (n) { case Nat.Zero { return v ; } case Nat.Succ(m) { return step(m, natInd(step,v,m)); } } +} + +function add(n: Nat, m: Nat) returns (Nat) { + return natInd (lam (x, acc) {return Nat.Succ(acc) ; }, m, n); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/PeanoMatch.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/PeanoMatch.solc new file mode 100644 index 00000000..fb24c72b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/PeanoMatch.solc @@ -0,0 +1,5 @@ +enum Nat { Zero, Succ(Nat) } + +function foo(n: Nat) returns (Nat) { + match (n) { case Nat.Zero { return Nat.Succ(Nat.Zero) ; } case Nat.Succ(Nat.Succ(x)) { return x; } case x { return Nat.Zero; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/RefDeref.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/RefDeref.solc new file mode 100644 index 00000000..76836601 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/RefDeref.solc @@ -0,0 +1,10 @@ +trait Loadable { + function load(r: ref) returns (deref) ; +} + +trait Storable { + function store(r: ref, d: deref) returns () ; +} + +// haskell style class constraints +trait Ref where ref: Loadable, ref: Storable {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SimpleLambda.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SimpleLambda.solc new file mode 100644 index 00000000..827a11ca --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SimpleLambda.solc @@ -0,0 +1,22 @@ +function addWord(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +contract SimpleLambda{ + function f(z: word) public returns (word) { + let n = lam (x: word, y: word) { + return addWord(x,addWord(y,1)); + } ; + let m = lam (x: word) { + return addWord (z,x) ; + } ; + return m(n(1,0)); + } + function main() public returns (word) { + return f(40); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SingleFun.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SingleFun.solc new file mode 100644 index 00000000..e922f202 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SingleFun.solc @@ -0,0 +1,3 @@ +function id(x: word) returns (word) { + return x ; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Uncurry.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Uncurry.solc new file mode 100644 index 00000000..71c0423c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/Uncurry.solc @@ -0,0 +1,3 @@ +function uncurry(f: word, p: (word, word)) returns (word) { + match (p) { case (x, y) { return f(x,y); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/abigeneric.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/abigeneric.solc new file mode 100644 index 00000000..22f154fa --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/abigeneric.solc @@ -0,0 +1,95 @@ +pragma solcore noPattersonCondition ABIAttribs, ABIEncode, ABIDecode; +pragma solcore noBoundVariableCondition ABIAttribs, ABIEncode, ABIDecode; +pragma solcore noCoverageCondition ABIDecode; + +export { + encode, + decode +}; + +import std; +import {mstore} from std.opcodes; +import std.Generic; + +function maxWord(a: word, b: word) returns (word) { + match (gtWord(a, b)) { case true { return a; } case false { return b; } } +} + +// ─── ABIAttribs for the primitive sum(f, g) type ───────────────────────── +// headSize = 32 (tag word) + max(headSize(f), headSize(g)) + +impl ABIAttribs> where f: ABIAttribs, g: ABIAttribs { + function headSize(ty: Proxy>) returns (word) { + let pf: Proxy; + let pg: Proxy; + return 32 + maxWord(ABIAttribs.headSize(pf), ABIAttribs.headSize(pg)); + } + function isStatic(ty: Proxy>) returns (bool) { + let pf: Proxy; + let pg: Proxy; + return and(ABIAttribs.isStatic(pf), ABIAttribs.isStatic(pg)); + } +} + +// ─── ABIEncode for sum(f, g) ───────────────────────────────────────────── +// Wire layout (static sums only): +// [offset + 0 .. offset + 31] : tag word (0 = inl, 1 = inr) +// [offset + 32 .. ] : encoded branch payload + +impl ABIEncode> where f: ABIAttribs, f: ABIEncode, g: ABIAttribs, g: ABIEncode { + function encodeInto(x: sum, basePtr: word, offset: word, tail: word) returns (word) { + match (x) { case inl(v) { mstore(basePtr + offset, 0); + return ABIEncode.encodeInto(v, basePtr, offset + 32, tail); } case inr(v) { mstore(basePtr + offset, 1); + return ABIEncode.encodeInto(v, basePtr, offset + 32, tail); } } + } +} + +// ─── ABIDecode for sum(f, g) ───────────────────────────────────────────── +// Reads the tag word at headOffset; dispatches to f or g decoder at headOffset + 32. + +impl ABIDecode, reader>, sum> where reader: WordReader, f: ABIAttribs, ABIDecoder: ABIDecode, ABIDecoder: ABIDecode { + function decode(ptr: ABIDecoder, reader>, headOffset: word) returns (sum) { + match (ptr) { case ABIDecoder.ABIDecoder(rdr) { let tag = WordReader.read(WordReader.advance(rdr, headOffset)); + match (tag) { case 0 { let dec_f: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return inl(ABIDecode.decode(dec_f, headOffset + 32)); } default { let dec_g: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return inr(ABIDecode.decode(dec_g, headOffset + 32)); } } } } + } +} + +// ─── Default bridges: ABIAttribs and ABIEncode via Generic ─────────────── +// Any type 'a' with Generic(rep) inherits its ABI layout from rep. + +default impl ABIAttribs where a: Generic, rep: ABIAttribs { + function headSize(ty: Proxy) returns (word) { + let prx: Proxy; + return ABIAttribs.headSize(prx); + } + function isStatic(ty: Proxy) returns (bool) { + let prx: Proxy; + return ABIAttribs.isStatic(prx); + } +} + +default impl ABIEncode where a: Generic, rep: ABIAttribs, rep: ABIEncode { + function encodeInto(x: a, basePtr: word, offset: word, tail: word) returns (word) { + return ABIEncode.encodeInto(Generic.from(x), basePtr, offset, tail); + } +} + +// ─── Top-level generic encode function ─────────────────────────────────── +// Serialises any 'a' that has a Generic(rep) instance. +// Only the Generic instance is required — ABIEncode is resolved via the bridge. + +function encode(x: a, basePtr: word, offset: word, tail: word) returns (word) where a: Generic, rep: ABIAttribs, rep: ABIEncode { + let xrep: rep = Generic.from(x); + return ABIEncode.encodeInto(xrep, basePtr, offset, tail); +} + +// ─── Top-level generic decode function ─────────────────────────────────── +// Deserialises any 'a' that has a Generic(rep) instance. +// Only the Generic instance is required — ABIDecode is resolved via the bridge. + +function decode(ptr: ABIDecoder, headOffset: word) returns (a) where a: Generic, reader: WordReader, ABIDecoder: ABIDecode { + match (ptr) { case ABIDecoder.ABIDecoder(rdr) { let rep_ptr: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + return Generic.to(ABIDecode.decode(rep_ptr, headOffset)); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/another-subst.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/another-subst.solc new file mode 100644 index 00000000..357b26b2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/another-subst.solc @@ -0,0 +1,7 @@ +trait Foo {function foo(x: a) returns () ; } + +impl Foo<(a, b)> where a: Foo, b: Foo { + function foo(p: (a, b)) returns () { + match (p) { case (pa, pb) { Foo.foo(pa); Foo.foo(pb); } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/app.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/app.solc new file mode 100644 index 00000000..6b63139b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/app.solc @@ -0,0 +1,21 @@ +function app(f: c, x: a) returns (b) where c: invokable { + return invokable.invoke(f, x); +} + +enum t_id { t_id } + +impl invokable { + function invoke(self: t_id, x: word) returns (word) { + return x; + } +} + +function foo() returns (word) { + return app(t_id.t_id, 0); +} + +contract C { + function main() public returns (word) { + return foo(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/array.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/array.solc new file mode 100644 index 00000000..502d0b9f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/array.solc @@ -0,0 +1,111 @@ +pragma solcore noCoverageCondition TAdd; + +enum Zero {} +enum Succ {} + +trait TAdd {} +impl TAdd<(Zero, a), a> {} +impl TAdd<(Succ, a), Succ> where (b, a): TAdd {} + +trait Eq {} +impl Eq {} + +// this should work but doesnt: forall sizel sizer elem sizeout . (sizel, sizer):TAdd(sizeout) +// TODO: this panics during specialization +/* +forall sizel sizer elem sizeout pairSizelSizer . pairSizelSizer:Eq((sizel, sizer)), pairSizelSizer:TAdd(sizeout) => function concat(lhs:memory(array(sizel, elem)), rhs:memory(array(sizer, elem))) -> memory(array(sizeout, elem)) { + return memory(0) : memory(array(sizeout, elem)); // :D +} +*/ +enum Itself { ItselfRuntimeTag } + +enum array { array } +enum memory { memory(word) } + +trait IndexAccessible { + function set(self: self, ix: indexType, val: elementType) returns () ; + function at(self: self, ix: indexType) returns (elementType) ; +} + +trait ToWord { + function toWord(self: Itself) returns (word) ; +} + +impl ToWord { + function toWord(zero: Itself) returns (word) { return 0; } +} + +impl ToWord> where prev: ToWord { + function toWord(self: Itself>) returns (word) { + let previous: Itself = Itself.ItselfRuntimeTag; + let returnVal: word = ToWord.toWord(previous); + assembly { + returnVal := add(1, returnVal) + } + return returnVal; + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let val: word; + assembly { val := mload(ptr) } + return val; + } + function store(ptr: word, value: word) returns () { + assembly { mstore(ptr, value) } + } +} + +impl IndexAccessible memory, word, elem> where size: ToWord, elem: MemoryType { + function at(self: array memory, index: word) returns (elem) { + let sizeTag: Itself = Itself.ItselfRuntimeTag; + let sizeValue = ToWord.toWord(sizeTag); + + assembly { + if iszero(lt(index, sizeValue)) { + revert(0, 0) + } + } + + match (self) { case memory.memory(offset) { let x = offset; // can't use this inside the assembly block :-( + assembly { + index := add(x, mul(32, index)) + } + return MemoryType.load(index); } } + } + + function set(self: array memory, index: word, val: elem) returns () { + let sizeTag: Itself = Itself.ItselfRuntimeTag; + let sizeValue = ToWord.toWord(sizeTag); + + assembly { + if iszero(lt(index, sizeValue)) { + revert(0, 0) + } + } + + match (self) { case memory.memory(offset) { let x = offset; // can't use this inside the assembly block :-( + assembly { + index := add(x, mul(32, index)) + } + MemoryType.store(index, val); } } + } +} + + + +contract Array { + + function main() public returns (word) { + let arr: array>>>, word> memory = memory.memory(42); // = (1,2,3,4,5,6,7,8,9,10); + IndexAccessible.set(arr, 3, 33); + + return IndexAccessible.at(arr, 3); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-bool-lit.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-bool-lit.solc new file mode 100644 index 00000000..715f948a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-bool-lit.solc @@ -0,0 +1,13 @@ +// Yul has no boolean type: `true`/`false` are word literals (1/0). A literal +// `true` in an assembly block must type-check as `word`. Before the fix +// `tcYLit YulTrue/YulFalse` called `notImplemented`, crashing the compiler. +contract Test { + function main() public returns (word) { + let r: word = 0; + assembly { + let x := true + r := x + } + return r; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-uninit.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-uninit.solc new file mode 100644 index 00000000..104aab4d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-let-uninit.solc @@ -0,0 +1,15 @@ +// An uninitialized Yul `let x` must introduce the binding so that later +// assignments and reads of `x` resolve and are type-checked as `word`. +// Before the fix `tcYulStmt` dropped `YLet ns Nothing`, so `x` never entered +// the env and the read `r := x` failed to resolve. +contract Test { + function main() public returns (word) { + let r: word = 0; + assembly { + let x + x := add(1, 1) + r := x + } + return r; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-read.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-read.solc new file mode 100644 index 00000000..bd66cce2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-read.solc @@ -0,0 +1,8 @@ +contract C { + function main() returns (word) { + let res: word; + let foo: (word, word) = (1, 42); + match (foo) { case (v0, v1) { assembly { res := v1 } } } + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-write-read.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-write-read.solc new file mode 100644 index 00000000..29b1bb0f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/asm-match-tuple-write-read.solc @@ -0,0 +1,16 @@ +// After an assembly block writes to a pattern variable, subsequent code in the +// same match arm should read the written value (not the original tuple component). +// Runtime correctness of the write->read depends on ecSubst being updated after +// the assembly block (EmitHull.hs: emitStmt MastAsm, modify ecSubst). +contract C { + function main() returns (word) { + let res: word; + let foo: (word, word) = (0, 0); + match (foo) { case (v0, v1) { { + assembly { v1 := 42 } + let x: word = v1; + assembly { res := x } + } } } + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/assembly.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/assembly.solc new file mode 100644 index 00000000..363edce8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/assembly.solc @@ -0,0 +1,18 @@ +trait Mem { + function size(x: a) returns (word) ; +} + +impl Mem { + function size(x: word) returns (word) { + return 32; + } +} + +function foo() returns () { + let ptr: word; + let arg: word = 0; + let size = Mem.size(arg); + assembly { + ptr := add(32, size) + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bal.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bal.solc new file mode 100644 index 00000000..7b7798b1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bal.solc @@ -0,0 +1,58 @@ +enum Proxy { Proxy } +enum dict { dict(word, Proxy, Proxy) } +enum address { address(word) } +enum storage { storage(word) } + +enum IndexAP { IndexAP(m, idx, Proxy) } + +function wal(ref: dict storage, src: address, amt: word) returns () { + let wordProxy: Proxy = Proxy.Proxy; + let ip = IndexAP.IndexAP(ref, src, wordProxy); + Assign.assign(LVA.acc(ip), amt); +} + + +/* Expected: + +ip : IndexAP(storage(dict(address, word)) , address, ?1) + +LVA.acc : forall self memberRefType. self:LVA(memberRefType) => self -> memberRefType + +instance IndexAP(storage(dict(index,member)), index, member):LVA(storage(member)) + + |- instance IndexAP(storage(map(address, word)), address, ?1) : LVA(storage(word))) where ?1 ~ word + +*/ + +/* Actual + +> Enter reduce() |- (?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) +> Reducing wanted constraints:(?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) using () +> After entailment:(?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) - () +>> Before eliminating equalities (?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) +>> After eliminating equalities:(IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4), ?l4 : Assign (word)) +>>> Found instance for:IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4) + +>>>Instance:?a5 ~ storage(?b5) => IndexAP(storage(dict(?c5, ?b5)), ?c5, ?b5) : LVA (?a5) !!! +>>>Subst:{?c5 +-> address, ?b5 +-> word, ?b5 +-> ?e4, ?l4 +-> ?a5} ??? + +b5 +-> e4 should really be b5 ~ e4 + +*/ +trait LVA { + function acc(x: self) returns (memberRefType) ; +} + +impl LVA storage, index, member>, member storage> { + function acc(x: IndexAP storage, index, member>) returns (member storage) { + return storage.storage(30); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign { + function assign(l: a storage, y: a) returns () {} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bar.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bar.solc new file mode 100644 index 00000000..cdb69b8b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bar.solc @@ -0,0 +1,18 @@ +pragma solcore noCoverageCondition Bar; + +enum Wrap { Wrap(a) } + +trait Foo {} + +trait Bar {} + +impl Bar, b> where a: Foo {} + +function need_bar(x: Wrap) returns () where Wrap: Bar { + return (); +} + +function use_bar(x: Wrap) returns () where a: Foo { + need_bar(x); + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bitwise.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bitwise.solc new file mode 100644 index 00000000..2a70ed40 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bitwise.solc @@ -0,0 +1,25 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +// Exercises the `^` / `&` / `|` operators, the `^=` / `&=` / `|=` +// compound assignments, and the bxorWord / bandWord / borWord constant +// folding (mirrors gtWord). +function fxor(x: word, y: word) returns (word) { + let acc: word = x ^ y; + acc ^= x; // acc = (x ^ y) ^ x == y + return acc ^ 0; // identity: a ^ 0 == a +} + +function fbitwise(x: word, y: word) returns (word) { + let acc: word = x & y; + acc |= x; // acc = (x & y) | x == x + acc &= y; // acc = x & y + return acc | 0; // identity: a | 0 == a +} + +contract Bitwise { + // fxor(5, 3) == 3, fbitwise(6, 3) == 2, 3 ^ 2 == 1 — folded at compile time. + function main() public returns (word) { return fxor(5, 3) ^ fbitwise(6, 3); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bool-elim.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bool-elim.solc new file mode 100644 index 00000000..88e56ba5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bool-elim.solc @@ -0,0 +1,11 @@ +enum Bool { False, True } + + function second(x: Bool, y: word) returns (word) { + match (x, y) { case (Bool.True, z) { return z; } case (Bool.False, z) { return z; } } + } + +contract Second { + function main() public returns (word) { + return second(Bool.True, 42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-merge-case.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-merge-case.solc new file mode 100644 index 00000000..9b491034 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-merge-case.solc @@ -0,0 +1,5 @@ +// Pragmas to disable checks for specific classes +//pragma no-bounded-variable-condition TestClassB1; + +// === Test Classes === +trait TestClassP1 {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-with-pragma.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-with-pragma.solc new file mode 100644 index 00000000..943b7457 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bound-with-pragma.solc @@ -0,0 +1,14 @@ +// Same test but with pragma to disable bound variable check +// This SHOULD PASS + +pragma solcore noBoundVariableCondition TestBound; +pragma solcore noPattersonCondition TestBound; // Also disable Patterson to avoid that error + +trait TestBound {} +trait TestHelper {} + +enum TestType { TestType } + +// Variable 'bad' appears in context but not in instance head +// But pragma disables the check, so should pass +impl TestBound> where bad: TestHelper {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-import-default-inst-shadow.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-import-default-inst-shadow.solc new file mode 100644 index 00000000..4a2226ab --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-import-default-inst-shadow.solc @@ -0,0 +1,31 @@ +pragma solcore noPattersonCondition ABIAttribs, ABIEncode; +pragma solcore noBoundVariableCondition ABIAttribs, ABIEncode; + +import std; +import std.Generic; + +// Minimal reproducer for the "imported-default-instance-stub mis-tagged" bug. +// +// std/Generic.solc exports: +// forall a rep . a:Generic(rep), rep:ABIAttribs, rep:ABIEncode => +// default instance a : ABIEncode { function encodeInto ... } +// +// This file redefines the exact same default instance locally. +// The instance head (True, "ABIEncode", [], TyVar "a") is shared. +// +// Bug path: +// 1. filterImportedInstanceConflicts uses topDeclClassNames, which returns [] +// because this file defines no class -- only instances. The imported stub +// is NOT filtered. +// 2. moduleInferenceDeclSegmentByKey maps the shared key to ModuleLocalDecl +// (the local definition arrives first in the ordered list). +// 3. retagModuleInferenceDecls retags the imported stub with the same key, +// giving it ModuleLocalDecl / CheckTopDeclBody mode. +// 4. tcTopDeclWithVisibility calls tcTopDecl' on the stub (funs = []). +// 5. tcInstance' -> checkCompleteInstDef -> "Incomplete definition for ABIEncode". + +default impl ABIEncode where a: Generic, rep: ABIAttribs, rep: ABIEncode { + function encodeInto(x: a, basePtr: word, offset: word, tail: word) returns (word) { + return ABIEncode.encodeInto(Generic.from(x), basePtr, offset, tail); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-rep-name-capture.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-rep-name-capture.solc new file mode 100644 index 00000000..a184510c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/bug-rep-name-capture.solc @@ -0,0 +1,24 @@ +// Bug: local variable named `rep` causes name capture with the type variable `rep` +// from `class abs : Typedef(rep)`. In NameResolution.hs, the S.ExpVar and +// S.ExpName cases used a wildcard `_` for the qualifier in patterns like +// `(_, Just TLocalVar)`, so a qualified call `Typedef.rep(a)` resolved to the +// local variable `rep` instead of the class method. +// +// Expected: compiles successfully; `Typedef.rep` resolves to the class method. +// Actual (before fix): PANIC: no resolution found for invokable.invoke + +import std; +import std.dispatch; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract Bug { + constructor() {} + + function f(a: uint256) returns (uint256) { + let rep: uint256 = a; + let w: word = Typedef.rep(a); + return Typedef.abs(w); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/catch-all.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/catch-all.solc new file mode 100644 index 00000000..93ae9dc2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/catch-all.solc @@ -0,0 +1,11 @@ +enum Bool { False, True } + +contract CatchAll { + function catchAll(x: Bool, y: Bool) public returns (Bool) { + match (x, y) { case (Bool.True, Bool.True) { return Bool.True; } case (z, w) { return z; } } + } + + function main() public returns (Bool) { + return catchAll(Bool.True, Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/class-context.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/class-context.solc new file mode 100644 index 00000000..85444e7e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/class-context.solc @@ -0,0 +1,3 @@ +trait CStructField { + function offsetSize(s: self) returns (word) ; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-capture-only.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-capture-only.solc new file mode 100644 index 00000000..199845cb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-capture-only.solc @@ -0,0 +1,7 @@ +function testApplied(x: word) returns (word) { + return x; +} + +function main() returns (word) { + return testApplied(1); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-bound-test.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-bound-test.solc new file mode 100644 index 00000000..49aa5b53 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-bound-test.solc @@ -0,0 +1,7 @@ +function foo(b: bool) returns () { + let y: word; + let f = lam (x: word) { + if (b) { let z: word = 7; y = z; } else {x = 1;} + }; + f(44); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-local.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-local.solc new file mode 100644 index 00000000..ac52f3f5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-local.solc @@ -0,0 +1,13 @@ +function test() returns (word) { + let f = lam (x: word) returns (word) { + let y: word = 42; + return y; + }; + return f(1); +} + +contract C { + function main() public returns (word) { + return test(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-std.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-std.solc new file mode 100644 index 00000000..ff101450 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var-std.solc @@ -0,0 +1,17 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract Bug { + function main() public returns (word) { + return makeClosure(42); + } + + function makeClosure(e: word) public returns (word) { + let f = lam (x: word) { + return e + x; // Uses Add.add typeclass method + }; + return f(1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var.solc new file mode 100644 index 00000000..a8f1d2c8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure-free-var.solc @@ -0,0 +1,29 @@ +function addW(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l,r) + } + return rw; +} + +trait Add { + function add(l: t, r: t) returns (t) ; +} + +impl Add { + function add(l: word, r: word) returns (word) { return addW(l,r); } +} + +contract Bug { + function main() public returns (word) { + return makeClosure(42); + } + + function makeClosure(e: word) public returns (word) { + let f = lam (x: word) { + return Add.add(x,e); // this crashes + // return addW(e,x); // this works + }; + return f(1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure.solc new file mode 100644 index 00000000..5e1cca18 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/closure.solc @@ -0,0 +1,7 @@ + function foo(z: word, k: (), a: word) returns (word) { + let f = lam (x: word, y: word) { + k; + return primAddWord(a,primAddWord(y,z)); + }; + return f(0,1); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/comparisons.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/comparisons.solc new file mode 100644 index 00000000..4f9b7362 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/comparisons.solc @@ -0,0 +1,17 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; +function f(x: word, y: word) returns (bool) { + return (!((x == y) + && (x != y) + && (x >= y) + && (x <= y) + || (x > y) + && (x < y) + )); +} + +contract Comparisons { + function main() public returns (bool) { return f(0,1); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/compose0.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/compose0.solc new file mode 100644 index 00000000..485e35fa --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/compose0.solc @@ -0,0 +1,5 @@ +function compose(f: function(b) returns (c), g: function(a) returns (b)) returns (function(a) returns (c)) { + return lam (x) { + return f(g(x)); + }; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/const.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/const.solc new file mode 100644 index 00000000..a36e76d0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/const.solc @@ -0,0 +1,9 @@ +function constApplied(x: word, y: word) returns (word) { + return y; +} + +contract Foo { + function main() public returns (word) { + return constApplied(0,1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance-context.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance-context.solc new file mode 100644 index 00000000..37575385 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance-context.solc @@ -0,0 +1,24 @@ + + +enum memory { memory(word) } + +trait ValueTy { + function rep(x: t) returns (word) ; +} + +impl ValueTy { + function rep(x: t memory) returns (word) { + match (x) { case memory.memory(w) { return w; } } + } +} + +trait Ref { + function store(loc: ref, value: deref) returns () ; +} + +impl Ref where t: ValueTy { + function store(loc: t memory, value: t) returns () { + // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... + let vw = ValueTy.rep(value); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance.solc new file mode 100644 index 00000000..01995c97 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constrained-instance.solc @@ -0,0 +1,23 @@ + +enum memory { memory(word) } + +trait ValueTy { + function rep(x: t) returns (word) ; +} + +impl ValueTy { + function rep(x: t memory) returns (word) { + match (x) { case memory.memory(w) { return w; } } + } +} + +trait Ref { + function store(loc: ref, value: deref) returns () ; +} + +impl Ref where t: ValueTy { + function store(loc: t memory, value: t) returns () { + // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... + let vw = ValueTy.rep(value); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constructor-weak-args.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constructor-weak-args.solc new file mode 100644 index 00000000..0ba6be42 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/constructor-weak-args.solc @@ -0,0 +1,7 @@ +trait Loadable { + function load(r: ref) returns (deref) ; +} + +function foo(v: t) returns (word) where t: Loadable { + return Loadable.load(v); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/copytomem.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/copytomem.solc new file mode 100644 index 00000000..2b98db7e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/copytomem.solc @@ -0,0 +1,12 @@ +enum MemoryWordReader { MemoryWordReader(word) } + +function copyToMem(reader: MemoryWordReader, dst: word, cnt: word) returns () { + match (reader) { case MemoryWordReader.MemoryWordReader(ptr) { assembly { mcopy(dst, ptr, cnt) } } } +} + +contract Main { + function main() public returns () { + let r: MemoryWordReader = MemoryWordReader.MemoryWordReader(42); + copyToMem(r, 0, 32); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs-inferred.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs-inferred.solc new file mode 100644 index 00000000..3c54829e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs-inferred.solc @@ -0,0 +1,12 @@ +function foo(x: word) returns (word) { + return bar(x); +} +function bar(x: word) returns (word) { + return foo(x); +} + +contract C { + function main() public returns (word) { + return foo(1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs.solc new file mode 100644 index 00000000..ce57434e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/cyclical-defs.solc @@ -0,0 +1,18 @@ +function foo(x: word) returns (word) { + return bar(x); +} +function bar(x: word) returns (word) { + return foo(x); +} + +contract C { + function m(x: word) public returns (word) { + return n(x); + } + function n(x: word) public returns (word) { + return m(x); + } + function main() public returns (word) { + return m(1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-excluded.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-excluded.solc new file mode 100644 index 00000000..f6d28908 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-excluded.solc @@ -0,0 +1,32 @@ +// Test: pragma no-generic-instance-for suppresses auto-derivation for the +// listed types. Pair has its instance suppressed and provided manually; +// Box gets its instance generated automatically. + +import std; +import std.Generic; + +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; +pragma solcore noGenericInstanceFor Pair; + +enum Pair { MkPair(a, b) } + +enum Box { MkBox(a) } + +// Manual instance for Pair (suppressed from auto-derivation). +impl Generic, (a, b)> { + function from(p: Pair) returns (a, b) { + match (p) { case Pair.MkPair(x, y) { return (x, y); } } + } + function to(t: (a, b)) returns (Pair) { + match (t) { case (x, y) { return Pair.MkPair(x, y); } } + } +} + +// Box gets its Generic instance auto-derived (not excluded). +function boxRoundtrip(v: word) returns (bool) { + let b: Box = Box.MkBox(v); + let r: word = Generic.from(b); + let b2: Box = Generic.to(r); + match (b2) { case Box.MkBox(v2) { return eqWord(v, v2); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-sum.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-sum.solc new file mode 100644 index 00000000..95797670 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/derive-generic-sum.solc @@ -0,0 +1,28 @@ +// Test: Generic instances are auto-derived for sum types. +// Neither Option nor Tree has an explicit Generic instance; both should be +// generated automatically by DeriveGeneric. + +import std; +import std.Generic; + +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +enum Option { None, Some(a) } + +enum Tree { Leaf, Node(Tree, a, Tree) } + +// Use the auto-derived instances to check that from/to round-trip. +function roundtripNone() returns (bool) { + let x: Option = Option.None; + let r: sum<(), word> = Generic.from(x); + let x2: Option = Generic.to(r); + match (x2) { case Option.None { return true; } case Option.Some(_) { return false; } } +} + +function roundtripSome(v: word) returns (bool) { + let x: Option = Option.Some(v); + let r: sum<(), word> = Generic.from(x); + let x2: Option = Generic.to(r); + match (x2) { case Option.None { return false; } case Option.Some(v2) { return eqWord(v, v2); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-assignment-context.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-assignment-context.solc new file mode 100644 index 00000000..200c16f8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-assignment-context.solc @@ -0,0 +1,7 @@ +enum Option { Some(a), None } + +function main() returns (Option) { + let x: Option; + x = Option.None; + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-call-arg-context.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-call-arg-context.solc new file mode 100644 index 00000000..9f565495 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-call-arg-context.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(word) } + +function use(x: Option) returns (word) { + match (x) { case Option.Some(v) { return v; } case Option.None { return 0; } } +} + +function main() returns (word) { + return use(Option.Some(7)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-constructor.solc new file mode 100644 index 00000000..6938a1d5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-constructor.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(word) } + +function mkSome(x: word) returns (Option) { + return Option.Some(x); +} + +function main() returns (word) { + match (mkSome(7)) { case Option.Some(v) { return v; } case Option.None { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-match-return.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-match-return.solc new file mode 100644 index 00000000..90f483df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-match-return.solc @@ -0,0 +1,9 @@ +enum Bar { Foo(word) } + +function x(x: Bar) returns (Bar) { + match (x) { case Bar.Foo(w) { return Bar.Foo(w); } } +} + +function main() returns (word) { + match (x(Bar.Foo(7))) { case Bar.Foo(w) { return w; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-nested-context.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-nested-context.solc new file mode 100644 index 00000000..317dfd57 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-expression-nested-context.solc @@ -0,0 +1,5 @@ +enum Option { Some(a), None } + +function main() returns (Option>) { + return Option.Some(Option.None); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-constructor.solc new file mode 100644 index 00000000..2d10c137 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-constructor.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(word) } + +function fromOption(x: Option) returns (word) { + match (x) { case Option.Some(v) { return v; } case Option.None { return 0; } } +} + +function main() returns (word) { + return fromOption(Option.Some(3)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-nested-constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-nested-constructor.solc new file mode 100644 index 00000000..ffa9fe7a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-pattern-nested-constructor.solc @@ -0,0 +1,9 @@ +enum Option { None, Some(a) } + +function join(mmx: Option>) returns (Option) { + match (mmx) { case Option.Some(Option.Some(x)) { return Option.Some(x); } default { return Option.None; } } +} + +function main() returns (word) { + match (join(Option.Some(Option.Some(9)))) { case Option.Some(v) { return v; } case Option.None { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-primitive-constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-primitive-constructor.solc new file mode 100644 index 00000000..0a868952 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/dot-primitive-constructor.solc @@ -0,0 +1,4 @@ +function main() returns (word) { + let b: bool = true; + match (b) { case true { return 1; } case false { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/empty-asm.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/empty-asm.solc new file mode 100644 index 00000000..bc46c9a8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/empty-asm.solc @@ -0,0 +1,5 @@ +function f(x: word) returns (word) { + match (x) { case 0 { let ret: word; + assembly {} + return ret; } default { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder.solc new file mode 100644 index 00000000..d365abc5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder.solc @@ -0,0 +1,33 @@ +enum TagA { TagA(word) } +enum TagB { TagB(word) } + +trait Tag { + function getTag(x: self) returns (rep) ; +} + +enum TypeA { TypeA(word) } +impl Tag { + function getTag(x: TypeA) returns (TagA) { + match (x) { case TypeA.TypeA(w) { return TagA.TagA(w); } } + } +} + +enum TypeB { TypeB(word) } +impl Tag { + function getTag(x: TypeB) returns (TagB) { + match (x) { case TypeB.TypeB(w) { return TagB.TagB(w); } } + } +} + +function tagFirst(x: a, y: b) returns (rep1) where a: Tag, b: Tag { + return Tag.getTag(x); +} + +contract C { + constructor() {} + + function main() public returns (word) { + let r: TagA = tagFirst(TypeA.TypeA(42), TypeB.TypeB(7)); + match (r) { case TagA.TagA(w) { return w; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder1.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder1.solc new file mode 100644 index 00000000..9c87a0bd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/encoder1.solc @@ -0,0 +1,24 @@ +import std; + +trait Encoder { + function encode(x: self, hint: word) returns (rep) ; +} + +enum Foo { Foo(word) } +impl Encoder { + function encode(x: Foo, hint: word) returns (word) { + match (x) { case Foo.Foo(w) { return w; } } + } +} + +function encodeAndDiscard(x: a) returns () where a: Encoder { + let enc: rep = Encoder.encode(x, 0); + return (); +} + +contract C { + function main() public returns (word) { + encodeAndDiscard(Foo.Foo(42)); + return 0; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/false-redundant-warning.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/false-redundant-warning.solc new file mode 100644 index 00000000..d92d0842 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/false-redundant-warning.solc @@ -0,0 +1,11 @@ +enum Bool { False, True } + +function test(x: Bool, y: Bool) returns (Bool) { + match (x, y) { case (Bool.True, z) { return z; } case (w, Bool.True) { return w; } case (a, b) { return b; } } +} + +contract FalseRedundantWarning { + function main() public returns (Bool) { + return test(Bool.False, Bool.True); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-helper-cxt-collision.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-helper-cxt-collision.solc new file mode 100644 index 00000000..248d0559 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-helper-cxt-collision.solc @@ -0,0 +1,14 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +enum FooCxt { FooCxt } + +contract Foo { + x: word; + + function get() public returns (word) { + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-name-error.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-name-error.solc new file mode 100644 index 00000000..1c450323 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/field-name-error.solc @@ -0,0 +1,12 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract PoC { + x : word; + + function main() public returns (word) { + return 0; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/foo-class.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/foo-class.solc new file mode 100644 index 00000000..7b67326d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/foo-class.solc @@ -0,0 +1,3 @@ +trait Foo { + function foo(x: self) returns (b) ; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-body-shadow.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-body-shadow.solc new file mode 100644 index 00000000..f93f0a5d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-body-shadow.solc @@ -0,0 +1,11 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract C { + function main() public returns (word) { + let x: word = 100; + let i: word = 0; + let s: word = 0; + for(i=0;i<=0;i=i+1) { let x: word = 1; s = x; } + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-break.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-break.solc new file mode 100644 index 00000000..a01bffbc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-break.solc @@ -0,0 +1,13 @@ +import {lt, Ord, Add, Sub, Bounded, Num, Eq, Typedef} from std; +contract BreakTest { + function main() public returns (word) { + let result: word = 0; + for (let i: word = 0; i < 10; i = i + 1) { + if (i == 5) { + break; + } else {} + result = result + 1; + } + return result; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-continue.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-continue.solc new file mode 100644 index 00000000..85c3e986 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-continue.solc @@ -0,0 +1,13 @@ +import {lt, Ord, Add, Sub, Bounded, Num, Eq, Typedef} from std; +contract ContinueTest { + function main() public returns (word) { + let result: word = 0; + for (let i: word = 0; i < 10; i = i + 1) { + if (i < 5) { + continue; + } else {} + result = result + 1; + } + return result; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-empty-init.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-empty-init.solc new file mode 100644 index 00000000..b2c829db --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-empty-init.solc @@ -0,0 +1,10 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract ForEmptyInit { + function main() returns (word) { + let i: word = 1; + let s = 0; + for(; i <= 10; i = i + 1) { s = s + i; } + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-init-shadow.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-init-shadow.solc new file mode 100644 index 00000000..b5a643f8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-init-shadow.solc @@ -0,0 +1,10 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract Prefor { + function main() public returns (word) { + let i: word = 100; + let s: word = 0; + for(let i=1;i<=10;i=i+1) { s = s + i; } + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-inner-block.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-inner-block.solc new file mode 100644 index 00000000..a033b37d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-inner-block.solc @@ -0,0 +1,10 @@ +import {lt, Ord, Add, Sub, Bounded, Num, Eq, Typedef} from std; +contract ForInner { + function main() public returns (word) { + let result: word = 0; + for (let height: word = 0; height < 7; height = height + 1) { + if (true) { result = height; } else {} + } + return result; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-let.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-let.solc new file mode 100644 index 00000000..deb62d98 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-let.solc @@ -0,0 +1,10 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract Prefor { + function main() public returns (word) { + let s: word = 0; + for(let i=1;i<=10;i=i+1) { s = s + i;} + + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-loop.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-loop.solc new file mode 100644 index 00000000..b8cd38b6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-loop.solc @@ -0,0 +1,11 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract Prefor { + function main() public returns (word) { + let i: word; + let s: word = 0; + for(i=1;i<=10;i=i+1) { s = s + i;} + + return s; + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-init.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-init.solc new file mode 100644 index 00000000..2fb0933e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-init.solc @@ -0,0 +1,12 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract ForMultiInit { + function main() returns (word) { + let i = 0; + let j = 0; + for (i = 1, j = 10; i <= 3; i = i + 1) { + j = j + i; + } + return j; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-post.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-post.solc new file mode 100644 index 00000000..a816f1c6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/for-multi-post.solc @@ -0,0 +1,11 @@ +import {Num, Add, Sub, Eq, Ord, Bounded, Typedef, le} from std; + +contract ForMultiPost { + function main() returns (word) { + let j = 0; + for (let i = 0; i <= 3; i = i + 1, j = j + 2) { + j = j + i; + } + return j; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg-synonym.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg-synonym.solc new file mode 100644 index 00000000..3c1ff1ae --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg-synonym.solc @@ -0,0 +1,10 @@ +alias W = word; + +function f(x: W) returns (W) { return x; } + +contract C { + + function main() public returns (word) { + return f(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg.solc new file mode 100644 index 00000000..378544a2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-pat-arg.solc @@ -0,0 +1,7 @@ +function g(x: word) returns (word) { return x; } + +function h(x: a) returns (a) { return x; } + +contract C { + function main() public returns (word) { return g(h(42)); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-variable-shadowing.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-variable-shadowing.solc new file mode 100644 index 00000000..b2854677 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/fresh-variable-shadowing.solc @@ -0,0 +1,11 @@ +enum Bool { False, True } + +function test(v0: Bool, p: Bool) returns (Bool) { + match (p) { case Bool.True { return Bool.False; } case z { return v0; } } +} + +contract FreshVariableShadowing { + function main() public returns (Bool) { + return test(Bool.True, Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/if-examples.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/if-examples.solc new file mode 100644 index 00000000..bd41e42e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/if-examples.solc @@ -0,0 +1,40 @@ +function toBool(x: word) returns (bool) { + match (x) { case 0 { return false; } default { return true; } } +} + +function gt(x: word, y: word) returns (bool) { + let res: word; + assembly { + res := gt(x,y) + } + return toBool(res); +} + +function max(x: word, y: word) returns (word) { + let res: word; + if (gt(x,y)) { + res = x; + } else { + res = y; + } + return res; +} + +function not(x: bool) returns (bool) { + if (x) { return false; } else { return true; } +} + +function foo(x: word) returns (bool) { + if (gt(x,0)) { + return true; + } else { + return false; + } +} + + +contract IfExamples { + function main() public returns (word) { + return ((not(foo(42)) ? 0 : 1)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/import-std.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/import-std.solc new file mode 100644 index 00000000..cd6576df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/import-std.solc @@ -0,0 +1,10 @@ +import * as std from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract Test { + function main() public returns (word) { + return std.addWord(21, 21); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/inc-closure.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/inc-closure.solc new file mode 100644 index 00000000..e7a19b88 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/inc-closure.solc @@ -0,0 +1,17 @@ +function inc(x: word) returns (word) { + let f = lam () { + let res: word; + assembly { + res := add(x,1) + } + return res; + } ; + return f(); +} + +contract Foo { + + function main() public returns (word) { + return inc(0); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-closure-error.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-closure-error.solc new file mode 100644 index 00000000..eacb5713 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-closure-error.solc @@ -0,0 +1,11 @@ +trait CtFun { + function ct(x: t) returns (function(t) returns (t)) ; +} + +impl CtFun { + function ct(x: word) returns (function(word) returns (word)) { + return lam (y: word) { + return x; + }; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym-int.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym-int.solc new file mode 100644 index 00000000..587c226f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym-int.solc @@ -0,0 +1,17 @@ +alias W = word; + +trait FromWord { + function fromWord(x: word) returns (i) ; +} + +impl FromWord { + function fromWord(x: word) returns (word) { return x; } +} + +contract C { + + function main() public returns (W) { + let r: W = FromWord.fromWord(42); + return r; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym.solc new file mode 100644 index 00000000..ab816fbb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/instance-synonym.solc @@ -0,0 +1,17 @@ +alias W = word; + +trait IdTy { + function id(x: self) returns (self) ; +} + +impl IdTy { + function id(x: W) returns (W) { + return x; + } +} + +contract C { + function main() public returns (word) { + return IdTy.id(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/invokable-issue.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/invokable-issue.solc new file mode 100644 index 00000000..3da2e7cd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/invokable-issue.solc @@ -0,0 +1,12 @@ +trait Typedef { + function abs(x: rep) returns (abs) ; + function rep(x: abs) returns (rep) ; +} + +/* default */ +impl Typedef { + function abs(x: t) returns (t) { return x; } + function rep(x: t) returns (t) { return x; } +} + +function lift1ac(f: function(rep) returns (res), x: rep) returns (res) where abs: Typedef { return f(Typedef.rep(x)); } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ixa.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ixa.solc new file mode 100644 index 00000000..10851c4c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ixa.solc @@ -0,0 +1,139 @@ +// --- preamble / duplicated std defs --- + +enum Proxy { Proxy } + +// dynamic arrays with a runtime size. cannot exist on stack so no data constructor (i.e. should be used in combination with memory / storage pointers). +enum array {} + +// a typed pointer to a location in memory +enum memory { memory(word) } + +// word arithmetc +trait Add { function add(l: t, r: t) returns (t) ; } +trait Mul { function mul(l: t, r: t) returns (t) ; } +impl Add { + function add(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l,r) + } + return rw; + } +} +impl Mul { + function mul(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := mul(l,r) + } + return rw; + } +} + +// --- MemoryType --- + +trait MemoryType { + function load(loc: word) returns (a) ; + function store(loc: word, val: a) returns () ; + function size(prx: Proxy) returns (word) ; +} + +impl MemoryType { + function load(loc: word) returns (word) { + let ret: word; + assembly { ret := mload(loc) } + return ret; + } + + function store(loc: word, val: word) returns () { + assembly { mstore(loc,val) } + } + + function size(prx: Proxy) returns (word) { + return 32; + } +} + +impl MemoryType memory> { + function load(loc: word) returns (array memory) { + let ret: word; + assembly { ret := mload(loc) } + return memory.memory(ret); + } + + function store(loc: word, val: array memory) returns () { + match (val) { case memory.memory(ptr) { assembly { mstore(loc,ptr) } } } + } + + function size(prx: Proxy) returns (word) { + return 32; + } +} + +// --- Assignment --- + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +impl Assign { + function assign(ptr: word memory, val: word) returns () { + match (ptr) { case memory.memory(loc) { assembly { + mstore(loc, val) + } } } + } +} + +// --- Index Access --- + +trait RValueIdxAccess { + function lookup(ci: col_idx) returns (val) ; +} + +trait LValueIdxAccess { + function lookup(ci: col_idx) returns (val) ; +} + +impl RValueIdxAccess<(array memory, word), a> where a: MemoryType { + function lookup(col_idx: (array memory, word)) returns (a) { + let elementProxy: Proxy = Proxy.Proxy; + let sz = MemoryType.size(elementProxy); + match (col_idx) { case (col, idx) { match (col) { case memory.memory(loc) { return MemoryType.load(Add.add(loc, Mul.mul(idx, sz))); } } } } + } +} + +impl LValueIdxAccess<(array memory, word), a memory> where a: MemoryType { + function lookup(col_idx: (array memory, word)) returns (a memory) { + let elementProxy: Proxy = Proxy.Proxy; + let sz = MemoryType.size(elementProxy); + match (col_idx) { case (col, idx) { match (col) { case memory.memory(loc) { return memory.memory(Add.add(loc, Mul.mul(idx, sz))); } } } } + } +} + +// --- Examples --- + +function main() returns () { + let x: array memory> memory = memory.memory(0); + let y: word = 0; + let z: array memory = memory.memory(0); + + let i0: word = 0; + let i1: word = 1; + let i2: word = 2; + let i3: word = 3; + let i4: word = 4; + let i5: word = 5; + + // y = z[0] + y = RValueIdxAccess.lookup((z, i0)); + + //y = x[0][1] + y = RValueIdxAccess.lookup((RValueIdxAccess.lookup((x, i0)), i1)); + + //x[2][3] = x[5][4] + Assign.assign( + // TODO: R or L for the x[2] lookup? + LValueIdxAccess.lookup((RValueIdxAccess.lookup((x, i2)), i3)), + RValueIdxAccess.lookup((RValueIdxAccess.lookup((x, i5)), i4)) + ); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/join.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/join.solc new file mode 100644 index 00000000..78100fa8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/join.solc @@ -0,0 +1,18 @@ +contract Option { + enum Option { None, Some(a) } + enum Bool { False, True } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + let result = Option.None; + match (mmx) { case Option.Some(Option.Some(x)) { result = Option.Some(x); } case Option.None { result = Option.None; } case Option.Some(Option.None) { result = Option.None; } default { result = Option.None; } } + return result; + } + + function main() public returns (word) { + return maybe(0, join(Option.Some(Option.Some(0)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/listid.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/listid.solc new file mode 100644 index 00000000..7fabf481 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/listid.solc @@ -0,0 +1,9 @@ +enum List { Nil, Cons(a, List) } + +function id(x: a) returns (a) { + return x; +} + +function listid(xs: List) returns (List) { + match (xs) { case List.Nil { return List.Nil ; } case List.Cons(x, xs) { return List.Cons(id(x), listid(xs)); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltimp.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltimp.solc new file mode 100644 index 00000000..c71d6c55 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltimp.solc @@ -0,0 +1,5 @@ +import {ltproxy} from ltproxy; + +contract LtImp { + function main() public returns (bool) { return ltproxy(); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltproxy.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltproxy.solc new file mode 100644 index 00000000..70a1a353 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/ltproxy.solc @@ -0,0 +1,7 @@ +import {lt} from std; +export { ltproxy }; + +function ltproxy() returns (bool) { + let zero: word = 0; + return (zero < 42); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-bitwise.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-bitwise.solc new file mode 100644 index 00000000..576a226b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-bitwise.solc @@ -0,0 +1,23 @@ +import std; +import {mstore} from std.opcodes; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +// Regression for the `|` ambiguity between the bitwise-or operator and the +// match-arm separator. Each arm below ends in a *bare* expression statement +// (no trailing `;`), which is exactly the shape that previously made the +// parser read `mstore(...) | => ...` as a single bitwise-or +// expression and break the `match`. The `|` *inside* the parentheses is a +// genuine bitwise-or; the `|` that starts each arm is a separator. +function emit(x: word) returns () { + match (x) { case 0 { mstore(0, x | 1); } case 1 { mstore(0, x & 1); } default { mstore(0, x); } } +} + +contract MatchBitwise { + // `0 | 1` still folds to 1 at the top level. + function main() public returns (word) { + emit(0); + return 0 | 1; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-yul.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-yul.solc new file mode 100644 index 00000000..2827d060 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/match-yul.solc @@ -0,0 +1,12 @@ +enum Wrapper { Wrapper(word) } +contract C { + function main() public returns (word) { + return foo(Wrapper.Wrapper(1)); + } + function foo(w: Wrapper) public returns (word) { + let result: word; + match (w) { case Wrapper.Wrapper(ptr) { //let ptr2 : word = ptr; + assembly { result := calldataload(ptr) } } } + return result; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/memory.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/memory.solc new file mode 100644 index 00000000..57003093 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/memory.solc @@ -0,0 +1,10 @@ +enum Memory { Memory(word) } +enum Bytes { Bytes } + +function get_bytes() returns (Memory) { + let ptr: word; + assembly { + ptr := mload(0x40) + } + return Memory.Memory(ptr); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mod-example.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mod-example.solc new file mode 100644 index 00000000..bc795861 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mod-example.solc @@ -0,0 +1,7 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; +function foo(x: word, y: word) returns (word) { + return x % y; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modifier.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modifier.solc new file mode 100644 index 00000000..be08d317 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modifier.solc @@ -0,0 +1,21 @@ +contract C { + function add(x: word, y: word) public returns (word) { + let r: word; + assembly { + r := add(x, y) + } + return r; + } + + // modifier pattern: wrap add with before/after code + function modifiedAdd(x: word, y: word) public returns (word) { + // before solidity placeholder + let result = add(x, y); // Solidity's placeholder: _; + // after solidity placeholder + return result; + } + + function main() public returns (word) { + return modifiedAdd(2, 1); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modulo.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modulo.solc new file mode 100644 index 00000000..1732a475 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/modulo.solc @@ -0,0 +1,17 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +// Exercises the `%` operator and the `%=` compound assignment +// (the Mod class), plus the mod constant folding. +function f(x: word, y: word) returns (word) { + let acc: word = x % y; + acc %= y; // (x % y) % y == x % y once reduced + return acc; +} + +contract Modulo { + // 17 % 5 == 2, 2 % 5 == 2 — folded at compile time. + function main() public returns (word) { return f(17, 5); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/monomorphic-require.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/monomorphic-require.solc new file mode 100644 index 00000000..64760a9b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/monomorphic-require.solc @@ -0,0 +1,35 @@ +// This should trigger a warning and an error in the specialiser +// due to unability to resolve result type of require +import {uint256, lt, not, Eq, ne, Proxy, bytes4, string} from std; +import std.dispatch; + +function myrevert(offset: word, length: word) returns (a) { + assembly { + revert(offset, length) + } + +} +function require(cond: bool) returns () { + if (!cond) { + let reverted: () = myrevert(0,0); + } +} + +function callvalue() returns (uint256) { + let res: word; + assembly { + res := callvalue() + } + return uint256.uint256(res); +} + +contract Deposit { +function deposit() public returns () { + require(callvalue() != uint256.uint256(0)); + return (); + } + +function main() public returns () { + deposit(); +} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/morefun.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/morefun.solc new file mode 100644 index 00000000..ba04be73 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/morefun.solc @@ -0,0 +1,10 @@ +enum Proxy { Proxy } + +trait C { + function fun(p: Proxy) returns (word) ; +} + +function morefun(p: Proxy) returns (word) where t: C { + let proxy: Proxy = Proxy.Proxy; + return C.fun(proxy); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-both-templates.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-both-templates.solc new file mode 100644 index 00000000..4c345cdc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-both-templates.solc @@ -0,0 +1,32 @@ +// Tests that both Template A and Template B fire when the class has methods in +// both directions. Both should discover the same binding rep=word; the second +// application is idempotent (extSpSubst with the same binding is a no-op). + +enum Box { Box(word) } + +trait Convert { + function toRep(x: self) returns (rep) ; + function fromRep(x: rep) returns (self) ; +} + +impl Convert { + function toRep(x: Box) returns (word) { + match (x) { case Box.Box(w) { return w; } } + } + function fromRep(x: word) returns (Box) { + return Box.Box(x); + } +} + +function roundtrip(x: a) returns (a) where a: Convert { + let r: rep = Convert.toRep(x); + return Convert.fromRep(r); +} + +contract C { + constructor() {} + function main() public returns (word) { + let b: Box = roundtrip(Box.Box(99)); + match (b) { case Box.Box(w) { return w; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-chain-phantom.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-chain-phantom.solc new file mode 100644 index 00000000..590265a3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-chain-phantom.solc @@ -0,0 +1,48 @@ +// Tests resolveMPTCsFromPreds in a "chain" scenario: +// - f has phantom rep in its monotype (Foo -> ()) +// - inside f, encode returns a value of type rep +// - that value is passed to sink whose monotype is rep -> () +// +// Without resolveMPTCsFromPreds the SM substitution lacks rep=word when +// sink's specialisation name is being built, which would produce sink$rep +// (wrong) instead of sink$word (correct). + +enum Foo { Foo(word) } + +trait Encoder { + function encode(x: self, hint: word) returns (rep) ; +} + +trait Sink { + function sink(x: rep) returns () ; +} + +impl Encoder { + function encode(x: Foo, hint: word) returns (word) { + match (x) { case Foo.Foo(v) { return v; } } + } +} + +impl Sink { + function sink(x: word) returns () { + return (); + } +} + +// phantom rep: rep does not appear in f's argument or return type. +// Inside the body, encode returns rep and sink consumes rep. +// resolveMPTCsFromPreds must bind rep=word so that sink specialises +// to sink$word (not sink$rep). +function f(x: a) returns () where a: Encoder, rep: Sink { + let r: rep = Encoder.encode(x, 0); + Sink.sink(r); + return (); +} + +contract C { + constructor() {} + function main() public returns (word) { + f(Foo.Foo(42)); + return 0; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-guard-extras-concrete.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-guard-extras-concrete.solc new file mode 100644 index 00000000..e928c76a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-guard-extras-concrete.solc @@ -0,0 +1,27 @@ +// Tests the guard in resolveMPTCFromPreds that skips tryResolveMPTC when all +// extras are already fully concrete. Here rep is written as the concrete type +// `word` directly in the constraint, so freetv extras = [] and the function +// compiles through normal type inference without phantom variable discovery. + +enum Box { Box(word) } + +trait Unbox { + function unbox(x: self) returns (rep) ; +} + +impl Unbox { + function unbox(x: Box) returns (word) { + match (x) { case Box.Box(w) { return w; } } + } +} + +function extractWord(x: a) returns (word) where a: Unbox { + return Unbox.unbox(x); +} + +contract C { + constructor() {} + function main() public returns (word) { + return extractWord(Box.Box(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-multi-instance.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-multi-instance.solc new file mode 100644 index 00000000..6bdc90a0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-multi-instance.solc @@ -0,0 +1,39 @@ +// Tests that tryResolveMPTC selects the correct instance when multiple instances +// of the same class are registered in the resolution table. +// For getTag(Foo(1)): specmgu (Bar -> RepBar) (Foo -> freshV) fails (Bar != Foo), +// so only the Foo entry fires and rep is resolved to RepFoo. +// Similarly for getTag(Bar(2)) rep resolves to RepBar. + +enum Foo { Foo(word) } +enum Bar { Bar(word) } +enum RepFoo { RepFoo(word) } +enum RepBar { RepBar(word) } + +trait Tagged { + function tag(x: self) returns (rep) ; +} + +impl Tagged { + function tag(x: Foo) returns (RepFoo) { + match (x) { case Foo.Foo(w) { return RepFoo.RepFoo(w); } } + } +} + +impl Tagged { + function tag(x: Bar) returns (RepBar) { + match (x) { case Bar.Bar(w) { return RepBar.RepBar(w); } } + } +} + +function getTag(x: a) returns (rep) where a: Tagged { + return Tagged.tag(x); +} + +contract C { + constructor() {} + function main() public returns (word) { + let rf: RepFoo = getTag(Foo.Foo(1)); + let rb: RepBar = getTag(Bar.Bar(2)); + match (rf) { case RepFoo.RepFoo(w) { return w; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-nop-mainty-free.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-nop-mainty-free.solc new file mode 100644 index 00000000..8383883d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-nop-mainty-free.solc @@ -0,0 +1,37 @@ +// Documents the NOP-A guard in resolveMPTCsFromPreds. +// +// The guard `null (freetv mainTy')` is false when the main type variable +// is not yet bound in the SM substitution. This happens for higher-order +// polymorphic functions that are specialised from the outside. +// +// Here `mapEncode` is only ever called with a concrete `a=Foo`, so at every +// call site the SM substitution has a=Foo before the body is processed. +// However, if `mapEncode` were called with an unresolved type the guard +// would fire and tryResolveMPTC would be skipped. +// +// This is a compile-only test: it verifies that the NOP-A guard does NOT +// interfere with the normal specialisation of `mapEncode` when called +// from a concrete call site. + +enum Foo { Foo(word) } + +trait Encoder { + function encode(x: self, hint: word) returns (rep) ; +} + +impl Encoder { + function encode(x: Foo, hint: word) returns (word) { + match (x) { case Foo.Foo(v) { return v; } } + } +} + +function extractVal(x: a) returns (rep) where a: Encoder { + return Encoder.encode(x, 0); +} + +contract C { + constructor() {} + function main() public returns (word) { + return extractVal(Foo.Foo(7)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-partial-instance.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-partial-instance.solc new file mode 100644 index 00000000..9d7fc8bc --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-partial-instance.solc @@ -0,0 +1,38 @@ +// Exercises the PARTIAL guard in tryResolveMPTC. +// +// The instance forall a b. instance Zero:Nth((a,b), a) has free type variables +// in its extras even after successfully matching Zero against the concrete main type. +// resolveMPTCsFromPreds detects this (concreteExtras still has free vars) and skips +// the instance, letting normal type inference determine the extra type instead. + +pragma solcore noCoverageCondition Nth; + +enum Zero {} +enum Succ {} +enum Proxy { Proxy } + +trait Nth { + function nth(x: Proxy, y: b) returns (c) ; +} + +impl Nth { + function nth(x: Proxy, y: (a, b)) returns (a) { + match (y) { case (a, b) { return a; } } + } +} + +impl Nth, (a, b), c> where n: Nth { + function nth(x: Proxy>, y: (a, b)) returns (c) { + match (y) { case (a, b) { let predecessorProxy: Proxy = Proxy.Proxy; return Nth.nth(predecessorProxy, b); } } + } +} + +contract C { + constructor() {} + function main() public returns (word) { + let p: (word, word, word) = (1, 2, 3); + let zeroProxy: Proxy = Proxy.Proxy; + let x: word = Nth.nth(zeroProxy, p); + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-a-only.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-a-only.solc new file mode 100644 index 00000000..1267f27b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-a-only.solc @@ -0,0 +1,27 @@ +// Tests tryResolveMPTC Template A path. +// The class has only a method of the form (self -> rep), so Template B cannot +// fire. The specialiser must discover rep=word solely via Template A: +// specmgu (Box -> word) (Box -> freshV) => freshV = word => rep = word + +enum Box { Box(word) } + +trait Unbox { + function unbox(x: self) returns (rep) ; +} + +impl Unbox { + function unbox(x: Box) returns (word) { + match (x) { case Box.Box(w) { return w; } } + } +} + +function extract(x: a) returns (rep) where a: Unbox { + return Unbox.unbox(x); +} + +contract C { + constructor() {} + function main() public returns (word) { + return extract(Box.Box(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-b-only.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-b-only.solc new file mode 100644 index 00000000..9fa43d15 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/mptc-template-b-only.solc @@ -0,0 +1,29 @@ +// Tests tryResolveMPTC Template B path. +// The class has only a method of the form (rep -> self), so Template A cannot +// fire. The specialiser must discover rep=word solely via Template B: +// specmgu (word -> Box) (freshV -> Box) => freshV = word => rep = word +// The `hint:a` argument makes a=Box concrete at the call site. + +enum Box { Box(word) } + +trait Rebox { + function rebox(x: rep) returns (self) ; +} + +impl Rebox { + function rebox(x: word) returns (Box) { + return Box.Box(x); + } +} + +function rewrap(val: rep, hint: a) returns (a) where a: Rebox { + return Rebox.rebox(val); +} + +contract C { + constructor() {} + function main() public returns (word) { + let b: Box = rewrap(7, Box.Box(0)); + match (b) { case Box.Box(w) { return w; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/multi-stmt-var-leaf.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/multi-stmt-var-leaf.solc new file mode 100644 index 00000000..f8debc70 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/multi-stmt-var-leaf.solc @@ -0,0 +1,8 @@ +enum Bool { False, True } + +contract MultiStmtVarLeaf { + function main(x: Bool) public returns (Bool) { + match (x) { case y { let z = y; + return z; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/nid.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/nid.solc new file mode 100644 index 00000000..4c06795e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/nid.solc @@ -0,0 +1,8 @@ +function id(x: word) returns (word) { + return x; +} + +function nid(x: word) returns (word) { + return id(x); +} + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/noclosure.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/noclosure.solc new file mode 100644 index 00000000..e67a282b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/noclosure.solc @@ -0,0 +1,6 @@ +function foo(z: word) returns (word) { + let f = lam (x: word, y: word) { + return primAddWord(x,primAddWord(y,1)); + }; + return primAddWord(f(0,1),z); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/notif.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/notif.solc new file mode 100644 index 00000000..2a114790 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/notif.solc @@ -0,0 +1,14 @@ +function not(x: bool) returns (bool) { + if (x) { + return false ; + } else { + return true ; + } +} + +function not2(x: bool) returns (bool) { + if (x) { + return false ; + } + return true; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/option2.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/option2.solc new file mode 100644 index 00000000..f7ce3401 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/option2.solc @@ -0,0 +1,22 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + match (mmx) { case Option.None { return Option.None; } case Option.Some(Option.None) { return Option.None; } case Option.Some(Option.Some(x)) { return Option.Some(x); } } + } + + function join2(mmx: Option>) public returns (Option) { + match (mmx) { case Option.Some(m) { match (m) { case Option.None { return Option.None; } case Option.Some(x) { return Option.Some(x); } } } default { return Option.None; } } + } + + function main() public returns (word) { + // return maybe(0, join(Option.Some(Option.Some(42)))); + return 42; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pair-bug.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pair-bug.solc new file mode 100644 index 00000000..beecb22b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pair-bug.solc @@ -0,0 +1,9 @@ +import std; + +contract TupleRet { + constructor() {} + + function pair() returns (uint256, uint256) { + return (uint256.uint256(7), uint256.uint256(11)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pars.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pars.solc new file mode 100644 index 00000000..5a82a094 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pars.solc @@ -0,0 +1,3 @@ +contract Pars { + function main() public returns () { let f: word; let value: word = 42; (); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/phantom-type-return-con.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/phantom-type-return-con.solc new file mode 100644 index 00000000..854f4b4b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/phantom-type-return-con.solc @@ -0,0 +1,14 @@ +enum Foo { Foo(word) } + function wrap(x: word) returns (Foo) { + return Foo.Foo(x); + } + + function unwrap() returns (word) { + match (wrap(42)) { case Foo.Foo(w) { return w; } } + } + + contract C { + function main() public returns (word) { + return unwrap(); + } + } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymatch-error.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymatch-error.solc new file mode 100644 index 00000000..db3aaf40 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymatch-error.solc @@ -0,0 +1,8 @@ +function fst(p: (a, b)) returns (a) { + match (p) { case (a, _) { return a; } } +} +contract TestUnitMatch { + function main() public returns () { + match ((), ()) { case (x, y) { return fst((x, y)); } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymorphic-require.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymorphic-require.solc new file mode 100644 index 00000000..fd13f873 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/polymorphic-require.solc @@ -0,0 +1,31 @@ +// This should trigger a warning and an error in the specialiser +// due to unability to resolve result type of require +import {uint256, lt, not, Eq, ne, Proxy, bytes4, string} from std; +import std.dispatch; + +function require(cond: bool) returns (a) { + if (!cond) { + assembly { + revert(0, 0) + } + } +} + +function callvalue() returns (uint256) { + let res: word; + assembly { + res := callvalue() + } + return uint256.uint256(res); +} + +contract Deposit { +function deposit() public returns () { + require(callvalue() != uint256.uint256(0)); + return (); + } + +function main() public returns () { + deposit(); +} +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_merge_base.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_merge_base.solc new file mode 100644 index 00000000..5325e196 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_merge_base.solc @@ -0,0 +1,47 @@ +// Test base file for pragma merging functionality +// This file contains violations of all three condition types with pragmas to disable checks + +// Pragmas to disable checks for specific classes +pragma solcore noPattersonCondition TestClassP1, TestClassB1, TestClassP3, TestClassB3; +pragma solcore noCoverageCondition TestClassC1, TestClassP3; +pragma solcore noBoundVariableCondition TestClassB1, TestClassB3; + +// --- Test Classes --- + +trait TestClassP1 {} +trait TestClassP2 {} +trait TestClassP3 {} + +trait TestClassC1 {} +trait TestClassC2 {} + +trait TestClassB1 {} +trait TestClassB2 {} +trait TestClassB3 {} + +// --- Data Types --- + +enum TestType1 { TestType1 } +enum TestType2 { TestType2 } + +// Fails Patterson: context constraint not smaller then head +impl TestClassP1 where (U, word): TestClassP1 {} + +// Patterson OK: No context predicates +impl TestClassP2 {} + +// --- Coverage Condition --- + +// Fails Coverage: Variable 'a' only appears in weak position (parameter to TestClassC1) +impl TestClassC1, a> {} + +// Coverage OK: All variables in strong positions +impl TestClassC2 {} + +// === Bound Variable Violations === + +// Fails Bound Variable & Patterson: Variable 'c' appears in context but not in instance head +impl TestClassB1, a> where c: TestClassB2 {} + +// Bound Variable OK: Simple instance without context +impl TestClassB2, TestType2> {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_test_patterson.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_test_patterson.solc new file mode 100644 index 00000000..91062406 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/pragma_test_patterson.solc @@ -0,0 +1,9 @@ +// Simple Patterson test - should fail without pragma + +trait C1 {} +trait C2 {} + +enum T { T } + +// This violates Patterson: context measure (2) >= conclusion measure (2) +impl C1> where U: C1, U: C2 {} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy-desugar.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy-desugar.solc new file mode 100644 index 00000000..45258b59 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy-desugar.solc @@ -0,0 +1,12 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function foo(x: @word) returns (word) { + return 0; +} + +function fuz(y: word) returns (word) { + return y + foo(@word); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy.solc new file mode 100644 index 00000000..ef0b34ee --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/proxy.solc @@ -0,0 +1,11 @@ +enum Proxy { Proxy } + +trait BaseMemoryType { + function memorySize(x: Proxy) returns (word) ; +} + + +function morefun(p: Proxy) returns (word) where t: BaseMemoryType { + let proxy: Proxy = Proxy.Proxy; + return BaseMemoryType.memorySize(proxy); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/rec.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/rec.solc new file mode 100644 index 00000000..9484450e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/rec.solc @@ -0,0 +1,3 @@ +function rec(n: word, b: word, f: word) returns (word) { + match (n) { case 0 { return b; } case m { return f(primAddWord(m,1), rec(m, b, f)); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/redundant-match.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/redundant-match.solc new file mode 100644 index 00000000..07e6cc71 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/redundant-match.solc @@ -0,0 +1,9 @@ +enum Bool { False, True } + + function f(x: Bool) returns (Bool) { + match (x) { case z { return z; } case Bool.True { return Bool.True; } case Bool.False { return Bool.False; } } + } + + contract Test { + function main() public returns (Bool) { return f(Bool.True); } + } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good.solc new file mode 100644 index 00000000..6e8b319b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good.solc @@ -0,0 +1,230 @@ + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + +impl Typedef { + function rep(x: word) returns (word) { return x; } + function abs(x: word) returns (word) { return x; } +} + +enum uint { uint(word) } + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + return Typedef.abs(MemoryType.load(ptr)); + } + function store(ptr: word, value: uint) returns () { + return MemoryType.store(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) returns () { + MemoryType.store(Typedef.rep(l), y); + } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +// This is *a lot* of pragmas... +pragma solcore noCoverageCondition CStructField, LValueMemberAccess, RValueMemberAccess; +pragma solcore noPattersonCondition LValueMemberAccess, RValueMemberAccess; +pragma solcore noBoundVariableCondition LValueMemberAccess, RValueMemberAccess; +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(Typedef.abs(ptr)); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + // BUG: Something wrong here? Complains about ptr not being word... + assembly { + ptr := add(ptr, size) + } + let result: fieldType = MemoryType.load(Typedef.abs(ptr)); + return result; + } +} + +////// Testing + +// struct S { x:word; y:uint; z:word; } +enum S { S(word, uint, word) } +enum x_sel { x_sel } +enum y_sel { y_sel } +enum z_sel { z_sel } + +impl CStructField, word, ()> {} +impl CStructField, uint, word> {} +// BUG: This next one should really be the following, but that breaks weirdly: +// (I get a patterson condition violation on an invoke instance for g) +// instance StructField(S, z_sel):CStructField(word, (word,uint)) {} +// So instead I use: +impl CStructField, word, word> {} + + +function f() returns () { + let x: word memory; + let y: word memory; + // x = y + Assign.assign(ref.ref(x), y); + /* + * Idea in the above: to avoid overlapping instances, + * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), + * to be able to choose a disjoint assign instance. + * Of course this needs special treatment during code generation, + * on the other hand, stack assignments generally do... + * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. + */ +} + +function g() returns () { + let s: S memory = Typedef.abs(0x80); + let y: word = 42; + let z: uint = uint.uint(42); + // s.x = y + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel)), y); + // s.y = 21 + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, y_sel.y_sel)), z); + // s.z = y; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), y); + // y = s.x + Assign.assign(ref.ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); + // s.z = s.x + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); +} +contract C { + function main() public returns () { + f(); + g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good1.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good1.solc new file mode 100644 index 00000000..a50f5002 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/reference-encoding-good1.solc @@ -0,0 +1,231 @@ + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +enum uint { uint(word) } + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum memory { memory(word) } +enum memoryRef { memoryRef(word) } +enum Proxy { Proxy } + +impl Typedef { + function rep(x: a memory) returns (word) { + match (x) { case memory.memory(y) { return y; } } + } + function abs(x: word) returns (a memory) { + return memory.memory(x); + } +} +impl Typedef, word> { + function rep(x: memoryRef) returns (word) { + match (x) { case memoryRef.memoryRef(y) { return y; } } + } + function abs(x: word) returns (memoryRef) { + return memoryRef.memoryRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait MemoryType { + function load(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait MemorySize { + function size(x: Proxy) returns (word) ; +} + +impl MemoryType { + function load(ptr: word) returns (word) { + let r: word; + assembly { + r := mload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + mstore(ptr, value) + } + } +} + +impl MemoryType { + function load(ptr: word) returns (uint) { + return Typedef.abs(MemoryType.load(ptr)); + } + function store(ptr: word, value: uint) returns () { + return MemoryType.store(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: MemoryType { + function assign(l: memoryRef, y: a) returns () { + MemoryType.store(Typedef.rep(l), y); + } +} + +impl Typedef { + function rep(x: word) returns (word) { return x; } + function abs(x: word) returns (word) { return x; } +} + + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +// This is *a lot* of pragmas... +pragma solcore noCoverageCondition CStructField, LValueMemberAccess, RValueMemberAccess; +pragma solcore noPattersonCondition LValueMemberAccess, RValueMemberAccess; +pragma solcore noBoundVariableCondition LValueMemberAccess, RValueMemberAccess; +trait CStructField {} +enum StructField { StructField(structType) } + +impl LValueMemberAccess, memoryRef> where StructField: CStructField, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (memoryRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return memoryRef.memoryRef(Typedef.abs(ptr)); + } +} + +impl MemorySize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + + +impl MemorySize { + function size(x: Proxy) returns (word) { + return 32; + } +} + +impl MemorySize<(a, b)> where a: MemorySize, b: MemorySize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = MemorySize.size(aProxy); + let b_sz: word = MemorySize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +impl RValueMemberAccess, fieldType> where StructField: CStructField, fieldType: MemoryType, offsetType: MemorySize { + function memberAccess(x: MemberAccessProxy) returns (fieldType) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = MemorySize.size(offsetProxy); + // BUG: Something wrong here? Complains about ptr not being word... + assembly { + ptr := add(ptr, size) + } + let result: fieldType = MemoryType.load(Typedef.abs(ptr)); + return result; + } +} + +////// Testing + +// struct S { x:word; y:uint; z:word; } +enum S { S(word, uint, word) } +enum x_sel { x_sel } +enum y_sel { y_sel } +enum z_sel { z_sel } + +impl CStructField, word, ()> {} +impl CStructField, uint, word> {} +// BUG: This next one should really be the following, but that breaks weirdly: +// (I get a patterson condition violation on an invoke instance for g) +// instance StructField(S, z_sel):CStructField(word, (word,uint)) {} +// So instead I use: +impl CStructField, word, word> {} + + +function f() returns () { + let x: word memory; + let y: word memory; + // x = y + Assign.assign(ref.ref(x), y); + /* + * Idea in the above: to avoid overlapping instances, + * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), + * to be able to choose a disjoint assign instance. + * Of course this needs special treatment during code generation, + * on the other hand, stack assignments generally do... + * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. + */ +} + +function g() returns () { + let s: S memory = Typedef.abs(0x80); + let y: word = 42; + let z: uint = uint.uint(42); + // s.x = y + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel)), y); + // s.y = 21 + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, y_sel.y_sel)), z); + // s.z = y; + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), y); + // y = s.x + Assign.assign(ref.ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); + // s.z = s.x + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, z_sel.z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(s, x_sel.x_sel))); +} +contract C { + function main() public returns () { + f(); + g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-adder.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-adder.solc new file mode 100644 index 00000000..18f99ef4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-adder.solc @@ -0,0 +1,20 @@ +// Returns a function with CORRECT type annotations. +// Validates the single-pass type checker: closure conversion must not hide +// that the returned lambda really has type (word) -> word. +// Uses an assembly block instead of primAddWord so it lowers end-to-end. +function makeAdder(x: word) returns (function(word) returns (word)) { + return lam (y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; + }; +} + +contract C { + function main() public returns (word) { + let f = makeAdder(10); + return f(5); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-const.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-const.solc new file mode 100644 index 00000000..a53fe113 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-const.solc @@ -0,0 +1,7 @@ +// Returns a constant function that closes over its argument. +// Correct annotations: (word) -> word, body returns the captured word. +function constFn(x: word) returns (function(word) returns (word)) { + return lam (y: word) returns (word) { + return x; + }; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-eq.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-eq.solc new file mode 100644 index 00000000..05a36740 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-eq.solc @@ -0,0 +1,18 @@ +// Returns a function comparing against a captured word, CORRECT annotations. +// Uses an assembly `eq` instead of primEqWord so it lowers end-to-end. +function makeEq(x: word) returns (function(word) returns (word)) { + return lam (y: word) returns (word) { + let res: word; + assembly { + res := eq(x, y) + } + return res; + }; +} + +contract C { + function main() public returns (word) { + let f = makeEq(7); + return f(7); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-instance.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-instance.solc new file mode 100644 index 00000000..9b74be6e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/return-fun-instance.solc @@ -0,0 +1,13 @@ +// Instance member returning a function with CORRECT annotations. +// The compiled-away validation pass used to check this; the single pass must too. +trait CtFun { + function ct(x: t) returns (function(t) returns (t)) ; +} + +impl CtFun { + function ct(x: word) returns (function(word) returns (word)) { + return lam (y: word) returns (word) { + return x; + }; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/same-name-constructor-qualifier.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/same-name-constructor-qualifier.solc new file mode 100644 index 00000000..77e6ef75 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/same-name-constructor-qualifier.solc @@ -0,0 +1,19 @@ +// Qualifier access (T.C) must work even when T has a same-name constructor. +// Regression test for: `Error.Empty` reporting "Unqualified constructor: Empty". +enum Err { Err(word), Empty, Msg(word) } + +function pickEmpty() returns (Err) { + return Err.Empty; +} + +function pickMsg(x: word) returns (Err) { + return Err.Msg(x); +} + +function pickErr(x: word) returns (Err) { + return Err.Err(x); +} + +function main() returns (word) { + match (pickEmpty()) { case Err.Empty { return 1; } case Err.Err(_) { return 2; } case Err.Msg(_) { return 3; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleDiscount.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleDiscount.solc new file mode 100644 index 00000000..a389c4cb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleDiscount.solc @@ -0,0 +1,18 @@ +// test complex match example from the blog post +// simplified to use word instead of uint256 + +import {address, Num, Add, Sub, Div, Bounded, Eq, Ord, Typedef} from std; + +enum AuctionState { NotStarted(word), Active(word, address), Ended(word, address), Cancelled(word, address) } + +enum Phase { Early, Late } + +function discount(state: AuctionState, phase: Phase) returns (word) { + match (state, phase) { case (AuctionState.Active(bid, _), Phase.Early) { return bid / 10; } case (AuctionState.Active(bid, _), Phase.Late) { return bid / 20; } default { return 0; } } +} + +contract Discount { + function main() public returns (word) { + return discount(AuctionState.Active(420, address.address(0)), Phase.Early); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleid.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleid.solc new file mode 100644 index 00000000..f344c110 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/simpleid.solc @@ -0,0 +1,3 @@ +function id(x: a) returns (a) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/single-lambda.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/single-lambda.solc new file mode 100644 index 00000000..a7b04509 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/single-lambda.solc @@ -0,0 +1,3 @@ +function foo() returns (function(word) returns (bool)) { + return lam (x: word) returns (bool) { return true; }; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/snds.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/snds.solc new file mode 100644 index 00000000..f4c3f7a7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/snds.solc @@ -0,0 +1,5 @@ + function snds(p1: (word, word), p2: (word, word)) returns (word, word) { + match (p1, p2) { case ((a, b), (c, d)) { return (b,d); } } + } + + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/spec-fail-ungrounded.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/spec-fail-ungrounded.solc new file mode 100644 index 00000000..8a168910 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/spec-fail-ungrounded.solc @@ -0,0 +1,27 @@ +// Specialiser rejects this program even though the type checker accepts it. +// +// abort_ : word -> a has a polymorphic return type (it diverges). +// sink_ : b -> word accepts any argument and discards it. +// +// At the call sink_(abort_(0)) the intermediate type 'a' (= 'b') is never +// pinned to a concrete type: +// - The type checker is satisfied because a type 'a' EXISTS that makes the +// program consistent (any type works); the overall expression has type word. +// - The specialiser needs a CONCRETE 'a' to emit code for abort_. It finds +// no constraint, no instance, and no return-type context to fix 'a', so +// ensureClosed reports a free type variable and aborts. + +function abort_(x: word) returns (a) { + return abort_(x); +} + +function sink_(y: b) returns (word) { + return 0; +} + +contract C { + constructor() {} + function main() public returns (word) { + return sink_(abort_(0)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/strange-unbound.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/strange-unbound.solc new file mode 100644 index 00000000..2fa817cb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/strange-unbound.solc @@ -0,0 +1,3 @@ +trait IsA { + function ais(p: (a, b)) returns (a) ; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/sum-match-default.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/sum-match-default.solc new file mode 100644 index 00000000..cf0e10d5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/sum-match-default.solc @@ -0,0 +1,12 @@ +contract SumMatchDefault { + enum Option { None, Some(a) } + + function g(s: Option) public returns (Option) { + match (s) { case Option.None { return Option.None; } case x { return x; } } + } + + function main() public returns (word) { + g(Option.None); + return 42; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-cycle.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-cycle.solc new file mode 100644 index 00000000..0ca228fd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-cycle.solc @@ -0,0 +1,14 @@ +trait A where a: B {} +trait B where a: A {} + +function needsB(x: a) returns () where a: B { + return (); +} + +function usesSuperCycle(x: a) returns () where a: A { + return needsB(x); +} + +function main() returns () { + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-num.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-num.solc new file mode 100644 index 00000000..5bfa70b7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class-num.solc @@ -0,0 +1,56 @@ +enum Bool { False, True } + +function fromBool(b: Bool) returns (word) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } +} + +function toBool(x: word) returns (Bool) { + match (x) { case 0 { return Bool.False; } default { return Bool.True; } } +} + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +impl Eq { + function eq(x: word, y: word) returns (Bool) { + let res: word; + assembly { + res := eq(x, y) + } + return toBool(res); + } +} + +function not(b: Bool) returns (Bool) { + match (b) { case Bool.True { return Bool.False ; } case Bool.False { return Bool.True ; } } +} + +function ne(x: a, y: a) returns (Bool) where a: Eq { + return not(Eq.eq(x,y)); +} + +trait Num where a: Eq { + function toWord(x: a) returns (word) ; + function fromWord(x: word) returns (a) ; +} + +impl Num { + function toWord(x: word) returns (word) { return x; } + function fromWord(x: word) returns (word) { return x; } +} + + +enum uint { uint(word) } + +impl Eq { + function eq(x: uint, y: uint) returns (Bool) { return Eq.eq(Num.toWord(x), Num.toWord(y)); } +} + + +impl Num { + function toWord(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function fromWord(x: word) returns (uint) { return uint.uint(x); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class.solc new file mode 100644 index 00000000..1e5f6bec --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/super-class.solc @@ -0,0 +1,26 @@ +enum List { Nil, Cons(a, List) } +enum Bool { False, True } + +function and(x: Bool, y: Bool) returns (Bool) { + match (x,y) { case (Bool.False, _) { return Bool.False; } case (Bool.True, y) { return y; } } +} + +trait Eq { + function eq(x: a, y: a) returns (Bool) ; +} + +impl Eq { + function eq(x: Bool, y: Bool) returns (Bool) { + match (x, y) { case (Bool.False, Bool.False) { return Bool.True; } case (Bool.True, Bool.True) { return Bool.True; } default { return Bool.False; } } + } +} + +impl Eq<(List)> where a: Eq { + function eq(xs: List, ys: List) returns (Bool) { + match (xs, ys) { case (List.Nil, List.Nil) { return Bool.True; } case (List.Cons(x, xs), List.Cons(y, ys)) { return and(Eq.eq(x,y),Eq.eq(xs,ys)); } default { return Bool.False; } } + } +} + +function foo() returns () { + let x = Eq.eq(List.Cons(Bool.True,List.Nil), List.Nil); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-basic.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-basic.solc new file mode 100644 index 00000000..e602b7a1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-basic.solc @@ -0,0 +1,19 @@ +alias Uint = word; +alias Point = pair; + +function useUint(x: Uint) returns (word) { + return x; +} + +function makePoint(x: word, y: word) returns (Point) { + return pair(x, y); +} + +function getX(p: Point) returns (word) { + match (p) { case pair(x, _) { return x; } } +} + +function main() returns (word) { + let p: Point = makePoint(10, 20); + return getX(p); +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-in-function.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-in-function.solc new file mode 100644 index 00000000..e08432f7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-in-function.solc @@ -0,0 +1,26 @@ +// Synonyms in function parameter and return types +alias Int = word; +alias Point = pair; + +function add(a: Int, b: Int) returns (Int) { + return a; +} + +function makePoint(x: Int, y: Int) returns (Point) { + return pair(x, y); +} + +function getX(p: Point) returns (Int) { + match (p) { case pair(x, _) { return x; } } +} + +function getY(p: Point) returns (Int) { + match (p) { case pair(_, y) { return y; } } +} + +function main() returns (word) { + let a: Int = 10; + let b: Int = 20; + let p: Point = makePoint(a, b); + return getX(p); +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-nested.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-nested.solc new file mode 100644 index 00000000..8ebd5578 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-nested.solc @@ -0,0 +1,22 @@ +// Deeply nested synonyms (synonym of synonym of synonym) +alias Word1 = word; +alias Word2 = Word1; +alias Word3 = Word2; + +alias Pair1 = pair; +alias Pair2 = Pair1; +alias Pair3 = Pair2; + +function useWord3(x: Word3) returns (word) { + return x; +} + +function usePair3(p: Pair3) returns (word) { + match (p) { case pair(x, _) { return x; } } +} + +function main() returns (word) { + let x: Word3 = 42; + let p: Pair3 = pair(1, 2); + return useWord3(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-param.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-param.solc new file mode 100644 index 00000000..09ce81fe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/synonym-param.solc @@ -0,0 +1,11 @@ +alias MyPair = pair; +alias IntPair = MyPair; + +function makePair(x: word, y: word) returns (MyPair) { + return pair(x, y); +} + +function main() returns (word) { + let p: IntPair = makePair(42, 100); + match (p) { case pair(x, _) { return x; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-default-instance.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-default-instance.solc new file mode 100644 index 00000000..c2b8dedd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-default-instance.solc @@ -0,0 +1,14 @@ +trait Fallback { + function tag(x: a) returns (word) ; +} + +default impl Fallback { + function tag(x: a) returns (word) { + return 7; + } +} + +function main() returns (word) { + let value: word = 0; + return Fallback.tag(value); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-given-order.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-given-order.solc new file mode 100644 index 00000000..23a01a3b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-given-order.solc @@ -0,0 +1,23 @@ +pragma solcore noPattersonCondition C; + +trait A {} +trait B {} +trait C {} + +impl C where a: A, a: B {} + +function needsC(x: a) returns () where a: C { + return (); +} + +function fromAB(x: a) returns () where a: A, a: B { + return needsC(x); +} + +function fromBA(x: a) returns () where a: B, a: A { + return needsC(x); +} + +function main() returns () { + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-residual-given.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-residual-given.solc new file mode 100644 index 00000000..ccc8b737 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tabled-residual-given.solc @@ -0,0 +1,18 @@ +pragma solcore noPattersonCondition Wanted; + +trait Known {} +trait Wanted {} + +impl Wanted where a: Known {} + +function needsWanted(x: a) returns () where a: Wanted { + return (); +} + +function passKnown(x: a) returns () where a: Known { + return needsWanted(x); +} + +function main() returns () { + return (); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/td.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/td.solc new file mode 100644 index 00000000..bf900e34 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/td.solc @@ -0,0 +1,18 @@ +trait Typedef { + function abs(x: rep) returns (abs) ; + function rep(x: abs) returns (rep) ; +} + +/* default */ +impl Typedef { + function abs(x: t) returns (t) { return x; } + function rep(x: t) returns (t) { return x; } +} + +function lift1ac(f: function(rep) returns (res), x: abs) returns (res) where abs: Typedef { return f(Typedef.rep(x)); } + + +function id(x: a) returns (a) { return x; } +contract TD { + function main() public returns (word) { return lift1ac(id, 42); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tiamat.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tiamat.solc new file mode 100644 index 00000000..8b92cfe6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tiamat.solc @@ -0,0 +1,128 @@ +enum Proxy { Proxy } +enum dict { dict(word, Proxy, Proxy) } +enum address { address(word) } +enum storage { storage(word) } + +function saddr(s: a storage) returns (word) { + match (s) { case storage.storage(a) { return a; } } +} + + +// Untyped Index (access) Proxy +enum UIP { UIP(m, idx) } +// Typed Index (access) Proxy +enum TIP { TIP(m, idx, Proxy) } + +function setbal(ref: dict storage, src: address, amt: word) returns () { + /* Based on inference: + ref : storage(dict(address, word)) + => ref[src] : storage(word) assuming src is of the right type + */ + let wordProxy: Proxy = Proxy.Proxy; + let tip = TIP.TIP(ref, src, wordProxy); + Assign.assign(LVA.acc(tip), amt); +} + +function setAllowance(ref: dict> storage, owner: address, spender: address, amt: word) returns () { + + let allowanceProxy: Proxy> = Proxy.Proxy; + let tip1: TIP> storage, address, dict> = TIP.TIP(ref, owner, allowanceProxy); + let ref2: dict storage = LVA.acc(tip1); + let wordProxy: Proxy = Proxy.Proxy; + let tip2: TIP storage, address, word> = TIP.TIP(ref2, spender, wordProxy); + let ref3: word storage = LVA.acc(tip2); + Assign.assign(ref3, amt); +} + +function getAllowance(ref: dict> storage, owner: address, spender: address) returns (word) { +/* + let tip : TIP(storage(dict(address, dict(address, word))), address, dict(address, word)) + = TIP(ref, owner, Proxy:Proxy(dict(address, word) )); + let ref2 : storage(dict(address,word)) = LVA.acc(tip); + let tip2 : TIP(storage(dict(address, word)), address, word) + = TIP(ref2, spender, Proxy:Proxy(word)); +*/ + let allowanceProxy: Proxy> = Proxy.Proxy; + let wordProxy: Proxy = Proxy.Proxy; + return RVA.acc( + TIP.TIP + ( LVA.acc( + TIP.TIP + (ref + , owner + , allowanceProxy + ) /* tip : TIP(storage(dict(address, dict(address, word))), address, dict(address, word)) */ + ) /* ref2 : storage(dict(address,word)) */ + , spender + , wordProxy + ) /* tip2 : TIP(storage(dict(address, word)), address, word) */ + ); +} + +trait LVA { + function acc(x: self) returns (memberRefType) ; +} + + +trait RVA { + function acc(x: self) returns (member) ; +} + +impl LVA storage, index, member>, member storage> { + function acc(x: TIP storage, index, member>) returns (member storage) { + return storage.storage(42); + } +} + +impl LVA storage, index, member>, member storage> { + function acc(x: UIP storage, index, member>) returns (member storage) { + return storage.storage(42); + } +} + +trait StorageType { + function sload(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +impl StorageType { + function sload(ptr: word) returns (word) { + let r: word; + assembly { + r := sload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + sstore(ptr, value) + } + } +} + +impl RVA storage, index, member>, member> where member: StorageType { + function acc(x: TIP storage, index, member>) returns (member) { + let addr = saddr(LVA.acc(x)); + return StorageType.sload(addr); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + + +impl Assign where a: StorageType { + function assign(l: a storage, r: a) returns () { + StorageType.store(saddr(l), r); + } +} + +contract Tiamat { + function main() public returns (word) { + let allowances: dict> storage; + let src = address.address(17); + setAllowance(allowances, address.address(1),address.address(2), 666); + return getAllowance(allowances, address.address(1),address.address(2)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuple-trick.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuple-trick.solc new file mode 100644 index 00000000..887b4f6c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuple-trick.solc @@ -0,0 +1,39 @@ +pragma solcore noCoverageCondition Nth; + + +enum Zero {} +enum Succ {} + +enum Proxy { Proxy } + +trait Nth { + function nth(x: Proxy, y: b) returns (c) ; +} + +impl Nth { + function nth(x: Proxy, y: (a, b)) returns (a) { + match (y) { case (a, b) { return a ; } } + } +} + +impl Nth, (a, b), c> where n: Nth { + function nth(x: Proxy>, y: (a, b)) returns (c) { + match (y) { case (a, b) { let predecessorProxy: Proxy = Proxy.Proxy; return Nth.nth(predecessorProxy, b); } } + } +} + +contract C { + function id(x: word) public returns (word) { + return x; + } + function main() public returns () { + let p: (word, word, word, ()); + let zeroProxy: Proxy = Proxy.Proxy; + let oneProxy: Proxy> = Proxy.Proxy; + let twoProxy: Proxy>> = Proxy.Proxy; + let x: word = Nth.nth(zeroProxy, p); + let y: word = Nth.nth(oneProxy, p); + let z: word = Nth.nth(twoProxy, p); + id(z); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuva.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuva.solc new file mode 100644 index 00000000..0437f190 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tuva.solc @@ -0,0 +1,73 @@ +// TUVA: TUple-based Value Access +/* +# Types and classes for assignemnt desugaring using +- access proxy types +- LValue and RValue access classes (LVA, RVA) +- StorageType class +- Assign class +*/ + +import {Typedef, storage, mapping, address, hash2, StorageType, Assign} from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + + +trait RValueIdxAccess { + function lookup(ci: col_idx) returns (val) ; +} + +trait LValueIdxAccess { + function lookup(ci: col_idx) returns (ref) ; +} + +impl LValueIdxAccess<(mapping(i => a) storage, i), a storage> where i: Typedef { + function lookup(xi: (mapping(i => a) storage, i)) returns (a storage) { + match (xi) { case (x, i) { return storage.storage(hash2(Typedef.rep(x), Typedef.rep(i))); } } + + // return storage(42); // FIXME: hash2(x,i); + } +} + +impl RValueIdxAccess<(mapping(i => a) storage, i), a> where a: StorageType, i: Typedef { + function lookup(xi: (mapping(i => a) storage, i)) returns (a) { + /* + match(xi) { + | (x, i) => return StorageType.load(hash2(Typedef.rep(x), Typedef.rep(i))); + } + */ + return readStorage(LValueIdxAccess.lookup(xi)); + } +} + +function readStorage(x: a storage) returns (a) where a: StorageType { + return StorageType.load(Typedef.rep(x)); +} + +function idx_rval(x: r) returns (a) where r: RValueIdxAccess { + return RValueIdxAccess.lookup(x); +} + +function idx_lval(x: r) returns (a) where r: LValueIdxAccess { + return LValueIdxAccess.lookup(x); +} + +contract TestTuva { + function main() public returns (word) { + let balances: mapping(address => word) storage; + let allowances: mapping(address => mapping(address => word)) storage; + let ref1: word storage = idx_lval( (balances, address.address(17)) ); + Assign.assign(idx_lval( (balances, address.address(1)) ), 1337); + + let ref2a // : storage( mapping(address, word) ) // omitting this type makes instance resolution fail + = idx_lval ( (allowances, address.address(1)) ); + + let ref2b // : storage( word ) + = idx_lval ( (ref2a, address.address(2)) ); + + Assign.assign( ref2b, 777 ); + +// return idx_rval( (balances, address(1)) ); + return idx_rval ( (ref2a, address.address(2)) ); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tyexp.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tyexp.solc new file mode 100644 index 00000000..29d95905 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/tyexp.solc @@ -0,0 +1,4 @@ +function main() returns (word) { + let y: word = 0; + return y; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/type-synonym-arg.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/type-synonym-arg.solc new file mode 100644 index 00000000..3c1ff1ae --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/type-synonym-arg.solc @@ -0,0 +1,10 @@ +alias W = word; + +function f(x: W) returns (W) { return x; } + +contract C { + + function main() public returns (word) { + return f(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/typedef.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/typedef.solc new file mode 100644 index 00000000..754ddce9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/typedef.solc @@ -0,0 +1,8 @@ +trait Typedef { + function rep(x: self) returns (underlyingType) ; + function abs(x: underlyingType) returns (self) ; +} + +function tripleFun(x: t) returns (word, (word, word)) where t: Typedef<(word, (word, word))> { + return Typedef.rep(x); + } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/uintdesugared.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/uintdesugared.solc new file mode 100644 index 00000000..b4970292 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/uintdesugared.solc @@ -0,0 +1,475 @@ +// Before desugaring: +/* +import IndexLib; + +contract Uint { + reserved : word; + owner : address; + decimals : uint; + totalSupply : uint; + balances : mapping(address,uint); + + function mint(amount:uint) { + balances[owner] = Num.add(balances[owner], amount); + totalSupply = Num.add(totalSupply, amount); + } + + function init() { + owner = address(0x123456789abcdef); + decimals = Num.fromWord(18); + } + function main() -> uint { + init(); + mint(uint(1000)); + mint(uint(1000)); + return balances[owner] : uint; + } +} +*/ + + +function addW(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +function subW(x: word, y: word) returns (word) { + let res: word; + assembly { + res := sub(x, y) + } + return res; +} + +function addU(x: uint, y: uint) returns (uint) { + let res: word; + let xw: word = Num.toWord(x); + let yw: word = Num.toWord(y); + assembly { + res := add(xw, yw) + } + return uint.uint(res); +} + +function hash1(x: word) returns (word) { + let result: word = 0; + assembly { + mstore(0, x) + result := keccak256(0,32) + } + return result; +} + +function hash2(x: word, y: word) returns (word) { + let result: word = 0; + assembly { + mstore(0, x) + mstore(32, y) + result := keccak256(0,64) + } + return result; +} + +trait Num { + function toWord(x: a) returns (word) ; + function fromWord(x: word) returns (a) ; + function add(x: a, y: a) returns (a) ; + function sub(x: a, y: a) returns (a) ; +} + +impl Num { + function toWord(x: word) returns (word) { return x; } + function fromWord(x: word) returns (word) { return x; } + function add(x: word, y: word) returns (word) { return addW(x, y); } + function sub(x: word, y: word) returns (word) { return addW(x, y); } +} + +enum uint { uint(word) } + +impl Num { + function toWord(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + + function fromWord(x: word) returns (uint) { return uint.uint(x); } + function add(x: uint, y: uint) returns (uint) { return uint.uint(addW(Num.toWord(x), Num.toWord(y))); } + function sub(x: uint, y: uint) returns (uint) { return uint.uint(subW(Num.toWord(x), Num.toWord(y))); } +} + +/* // this breaks the Paterson condition +forall a. a:Typedef(word) => +instance a:Num { + function toWord(x:a) -> word { return Typedef.rep(x); } + function fromWord(x:word) { return Typedef.abs(x); } + function add(x:a, y:a) -> a { return Typedef.abs(addW(Typedef.rep(x), Typedef.rep(y))); } +} +*/ + +// Storage slots and mapping access + + +/////// Construction +trait Typedef { + function rep(x: abs) returns (rep) ; + function abs(x: rep) returns (abs) ; +} + + +// this does not work :( +/* +forall a +. default instance a:Typedef(a) { + function rep(x:a) -> word { return a; } + function abs(x:a) -> word { return a;} +} +*/ + +impl Typedef { + function rep(x: word) returns (word) { return x; } + function abs(x: word) returns (word) { return x; } +} + +impl Typedef { + function rep(x: uint) returns (word) { + match (x) { case uint.uint(y) { return y; } } + } + function abs(x: word) returns (uint) { + return uint.uint(x); + } +} + +enum address { address(word) } + +impl Typedef { + function rep(x: address) returns (word) { + match (x) { case address.address(y) { return y; } } + } + function abs(x: word) returns (address) { + return address.address(x); + } +} + +enum storage { storage(word) } +enum ContractStorage { ContractStorage(cxt) } + +enum storageRef { storageRef(word) } +enum Proxy { Proxy } + +enum mapRef { mapRef(word) } //ref to a map elem + +// data memoryRef(a) = memoryRef(word); + +impl Typedef { + function rep(x: a storage) returns (word) { + match (x) { case storage.storage(y) { return y; } } + } + function abs(x: word) returns (a storage) { + return storage.storage(x); + } +} + +impl Typedef, word> { + function rep(x: storageRef) returns (word) { + match (x) { case storageRef.storageRef(y) { return y; } } + } + function abs(x: word) returns (storageRef) { + return storageRef.storageRef(x); + } +} + +trait Assign { + function assign(l: lhs, r: rhs) returns () ; +} + +enum ref { ref(a) } + +impl Assign, a> { + function assign(l: ref, r: a) returns () { + // builtin "stack store" + return (); + } +} + +trait StorageType { + function sload(ptr: word) returns (self) ; + function store(ptr: word, value: self) returns () ; +} + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + + +function sload_(x: word) returns (word) { + let res: word; + assembly { + res := sload(x) + } + return res; + } + +function sstore_(a: word, v: word) returns () { + assembly { sstore(a,v) } +} + +impl StorageType { + function sload(ptr: word) returns (word) { + let r: word; + assembly { + r := sload(ptr) + } + return r; + } + function store(ptr: word, value: word) returns () { + assembly { + sstore(ptr, value) + } + } +} + +impl StorageType { + function sload(ptr: word) returns (uint) { + let result: uint = Typedef.abs(sload_(ptr)); // explicit type annotation needed due to a typechecker bug + return result; + } + function store(ptr: word, value: uint) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} + +impl StorageType
{ + function sload(ptr: word) returns (address) { + let result: address = Typedef.abs(sload_(ptr)); // explicit type annotation needed due to a typechecker bug + return result; + } + function store(ptr: word, value: address) returns () { + return sstore_(ptr, Typedef.rep(value)); + } +} + +impl Assign, a> where a: StorageType { + function assign(l: storageRef, y: a) returns () { + StorageType.store(Typedef.rep(l), y); + } +} + +trait CStructField {} +enum StructField { StructField(structType) } + + +enum MemberAccessProxy { MemberAccessProxy(a, field) } + +function memberAccessD1(x: MemberAccessProxy) returns (a) { + match (x) { case MemberAccessProxy.MemberAccessProxy(y, z) { return y; } } +} + +trait LValueMemberAccess { + function memberAccess(x: self) returns (memberRefType) ; +} + +trait RValueMemberAccess { + function memberAccess(x: self) returns (memberValueType) ; +} + +impl LValueMemberAccess, storageRef> where StructField: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy) returns (storageRef) { + let ptr: word = Typedef.rep(memberAccessD1(x)); + let offsetProxy: Proxy = Proxy.Proxy; + let size: word = StorageSize.size(offsetProxy); + assembly { + ptr := add(ptr, size) + } + return storageRef.storageRef(ptr); + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize
{ + function size(x: Proxy
) returns (word) { + return 1; + } +} + + +/* +// fails Patterson cond +forall a b . a:Typedef(b), b:StorageSize +=> instance a:StorageSize { + function size(x:Proxy(a)) -> word { + return StorageSize.size(Proxy(b)); + } +} +*/ + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + assembly { + a_sz := add(a_sz, b_sz) + } + return a_sz; + } +} + +pragma solcore noPattersonCondition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances +pragma solcore noCoverageCondition MemberAccessProxy, LValueMemberAccess, RValueMemberAccess; + +// ------------------------------------------------------------------ +// Contract field access +// ------------------------------------------------------------------ + +impl LValueMemberAccess, fieldSelector, offsetType>, storageRef> where StructField, fieldSelector>: CStructField, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (storageRef) { + let ptr: word = 0x100; // forge uses at least 1 storage slot + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy); + + assembly { + ptr := add(ptr, offsetSize) + } + return storageRef.storageRef(ptr); // contract storage starts at 0 + } +} + +impl RValueMemberAccess, fieldSelector, offsetType>, fieldType> where StructField, fieldSelector>: CStructField, fieldType: StorageType, offsetType: StorageSize { + function memberAccess(x: MemberAccessProxy, fieldSelector, offsetType>) returns (fieldType) { + let ptr: word = 0x100; + let offsetProxy: Proxy = Proxy.Proxy; + let offsetSize: word = StorageSize.size(offsetProxy); + let result: fieldType = StorageType.sload(addW(ptr, offsetSize)); + return result; + } +} + +/* +forall cxt fieldSelector fieldType offsetType + . StructField(ContractStorage(cxt), fieldSelector):CStructField(fieldType, offsetType) + , fieldType:StorageType + , offsetType:StorageSize + => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { + function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { + let ptr:word = 0x100; + let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); + return StorageType.sload(addW(ptr, offsetSize)):fieldType; + } +} +*/ +// ------------------------------------------------------------------ +// Indexed access +// ------------------------------------------------------------------ + +enum mapping { mapping(word) } + +impl Typedef member), word> { + function rep(x: mapping(index => member)) returns (word) { + match (x) { case mapping.mapping(y) { return y; } } + } + function abs(x: word) returns (mapping(index => member)) { + return mapping.mapping(x); + } +} + + +// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays +impl StorageSize member)> { + function size(x: Proxy member)>) returns (word) { + return 1; + } +} + +enum IndexAccessProxy { IndexAccessProxy(map, index) } + +impl LValueMemberAccess, storageRef> where index: Typedef, map: Typedef { + function memberAccess(x: IndexAccessProxy) returns (storageRef) { + return storageRef.storageRef(indexStorageSlot(x)); + } +} + +impl RValueMemberAccess, member> where index: Typedef, member: StorageType, map: Typedef { + function memberAccess(x: IndexAccessProxy) returns (member) { + let slot: word = indexStorageSlot(x); + return StorageType.sload(slot); + } +} + +//function indexStorageSlot(x) +function indexStorageSlot(x: IndexAccessProxy) returns (word) where map: Typedef, index: Typedef { + match (x) { case IndexAccessProxy.IndexAccessProxy(map, i) { let mapptr: word = Typedef.rep(map); + let rawidx: word = Typedef.rep(i); + let loc: word = hash2(mapptr, rawidx); + return loc; } } +} + +/* +forall index map member. map:Typedef(word), index:Typedef(word) +=> function indexedSlot(mapref : storageRef(mapping(index, member)), i: index) -> word +{ + match mapref { + | storageRef(mapptr) => + let rawidx:word = Typedef.rep(i); + let loc:word = hash2(mapptr, rawidx); + return loc; + } +} +*/ + +function rval(x: a) returns (b) where a: RValueMemberAccess { + return RValueMemberAccess.memberAccess(x); +} + +enum UintCxt { UintCxt } +enum reserved_sel { reserved_sel } +impl CStructField, reserved_sel>, word, ()> { +} +enum owner_sel { owner_sel } +impl CStructField, owner_sel>, address, (word, ())> { +} +enum decimals_sel { decimals_sel } +impl CStructField, decimals_sel>, uint, (word, (address, ()))> { +} +enum totalSupply_sel { totalSupply_sel } +impl CStructField, totalSupply_sel>, uint, (word, (address, (uint, ())))> { +} +enum balances_sel { balances_sel } +impl CStructField, balances_sel>, mapping(address => uint), (word, (address, (uint, (uint, ()))))> { +} +contract Uint { + function mint(amount: uint) public returns () { + Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)))), Num.add(rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)))), amount)); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), totalSupply_sel.totalSupply_sel)), Num.add(rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), totalSupply_sel.totalSupply_sel)), amount)); + } + function init() public returns () { + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)), address.address(81985529216486895)); + Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), decimals_sel.decimals_sel)), Num.fromWord(18)); + } + function main() public returns (uint) { + init(); + mint(uint.uint(1000)); + mint(uint.uint(1000)); + let result: uint = rval(IndexAccessProxy.IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), balances_sel.balances_sel)), rval(MemberAccessProxy.MemberAccessProxy(ContractStorage.ContractStorage(UintCxt.UintCxt), owner_sel.owner_sel)))); + return result; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/undefined.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/undefined.solc new file mode 100644 index 00000000..2a12a5a1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/undefined.solc @@ -0,0 +1,13 @@ +function undefined() returns (any) { + assembly { + revert(0,0) + } +} + +function useWord(w: word) returns () {} + +contract Magic { + function main() public returns () { + useWord(undefined()); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/unit.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/unit.solc new file mode 100644 index 00000000..1840ecba --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/unit.solc @@ -0,0 +1,31 @@ +contract Unit { +function one(x: ()) public returns (word) { + return 1; +} + +function unitVal() public returns () { + return (); +} + +function unitMatch(x: ()) public returns (word) { + match (x) { case () { return 1; } } +} + +function foo(x: word) public returns () { + return (); +} + +function main() public returns (word) { + return unitMatch(foo(one(unitVal()))); +} +} + +trait Def { + function def() returns (a) ; +} + +impl Def<()> { + function def() returns () { + return (); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match-default.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match-default.solc new file mode 100644 index 00000000..4a76e4b4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match-default.solc @@ -0,0 +1,11 @@ +contract WordMatchDefault { + function f(n: word) public returns (word) { + let result: word; + match (n) { case 0 { assembly { result := 100 } } case x { assembly { result := x } } } + return result; + } + + function main() public returns (word) { + return f(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match.solc new file mode 100644 index 00000000..08e2261a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/word-match.solc @@ -0,0 +1,8 @@ + +trait IsWord { function toWord(x: a) returns (word) ; } + +function kw(a: word, b: word) returns (word) {return a;} + +function bar(x: (a, b)) returns (word) where a: IsWord, b: IsWord { + match (x) { case (t, u) { return kw(IsWord.toWord(t), IsWord.toWord(u)); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-for-body.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-for-body.solc new file mode 100644 index 00000000..208d05cd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-for-body.solc @@ -0,0 +1,16 @@ +import std; + +function yul_asm_for_body() returns () { + let result: word = 0; + assembly { + for { let i := 0 } lt(i, 3) { i := add(i, 1) } { + result := callvalue() + } + } +} + +contract Foo { + function main() public returns () { + return yul_asm_for_body(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-switch-body.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-switch-body.solc new file mode 100644 index 00000000..85c2b801 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-asm-switch-body.solc @@ -0,0 +1,17 @@ +import std; + +function yul_asm_switch_body() returns () { + let result: word = 0; + let flag: word = 1; + assembly { + switch flag + case 0 { result := 0 } + default { result := callvalue() } + } +} + +contract Foo { + function main() public returns () { + return yul_asm_switch_body(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-deposit-example.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-deposit-example.solc new file mode 100644 index 00000000..25d26427 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-deposit-example.solc @@ -0,0 +1,14 @@ +import std; + +function deposit(pubkey: string memory, withdrawal_credentials: string memory, signature: string memory, deposit_data_root: uint256) returns () { + let msg_value: word = 0; + assembly { + msg_value := callvalue() + } +} + +contract Foo { + function main() public returns () { + deposit(memory.memory(0), memory.memory(0), memory.memory(0), uint256.uint256(2)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-for.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-for.solc new file mode 100644 index 00000000..6aebaa80 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-for.solc @@ -0,0 +1,14 @@ +contract YulFor { + function main() public returns (word) { + let loopStart: word = 128; + let loopEnd: word = 256; + let res: word; + assembly { + let i := loopStart + for {} lt(i, loopEnd) { i := add(i, 32) } + { mstore(i, 42) } + res := mload(192) + } + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-function-typing.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-function-typing.solc new file mode 100644 index 00000000..68bf6430 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-function-typing.solc @@ -0,0 +1,8 @@ +function foo(length: word, pos: word) returns (word) { + let ret: word; + assembly { + // ret := add(pos, mul(0x20, iszero(iszero(length)))) + ret := iszero(iszero(length)) + } + return ret; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-multi-return.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-multi-return.solc new file mode 100644 index 00000000..1adbea22 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-multi-return.solc @@ -0,0 +1,18 @@ +// A Yul function with multiple named returns must keep its true return arity: +// 'x, y := pair()' assigns 2 values from a 2-return function and is valid Yul, +// so the type checker must accept it (regression for the arity check that used +// to collapse every non-empty return list to a single 'word'). +contract YulMultiRet { + function main() public returns (word) { + let x: word; + let y: word; + assembly { + function pair() -> a, b { + a := 1 + b := 2 + } + x, y := pair() + } + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-return.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-return.solc new file mode 100644 index 00000000..89cffcd1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/cases/yul-return.solc @@ -0,0 +1,7 @@ +contract C { + function main() public returns () { + assembly { + return(0,0) + } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondExpr.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondExpr.solc new file mode 100644 index 00000000..121eee7b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondExpr.solc @@ -0,0 +1,12 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function notAnswer(n: word) returns (word) { return ((n == 42) ? 0 : 42); } + +function answer(n: word) returns (word) { return notAnswer(notAnswer(42)); } + +contract Fib { + function main() public returns (word) { return answer(42); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondStmt.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondStmt.solc new file mode 100644 index 00000000..eea2fecf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/CondStmt.solc @@ -0,0 +1,17 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function notAnswer(n: word) returns (word) { + if(n == 42) { return 0; } else {return 42; } +} + +function answer(n: word) returns (word) { + return notAnswer(notAnswer(42)); +} +contract Fib { +function main() public returns (word) { + return answer(42); +} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/OneTwo.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/OneTwo.solc new file mode 100644 index 00000000..54058fe1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/OneTwo.solc @@ -0,0 +1,28 @@ +// This function should be in stdlib +function addWord(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l,r) + } + return rw; +} + + function zero() returns (word) { + return 0; + } + +function one() returns (word) { + return addWord(1, zero()) ; + } + +function two() returns (word) { + let x = zero(); + x = addWord(x, one()); + x = addWord(x,x); + return x; +} + +contract OneTwo { + function main() public returns (word) { return two(); } +} + diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Plus.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Plus.solc new file mode 100644 index 00000000..00b63b6a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Plus.solc @@ -0,0 +1,22 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + function zero() returns (word) { + return 0; + } + +function one() returns (word) { + return 1 + zero() ; + } + +function two() returns (word) { + let x = zero(); + x = x + one(); + x = x + x ; + return x; +} + +contract Plus { + function main() public returns (word) { return two() + two(); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Size.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Size.solc new file mode 100644 index 00000000..8615ccbf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Size.solc @@ -0,0 +1,49 @@ +enum Proxy { Proxy } + +function addWord(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l,r) + } + return rw; +} + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + + +default impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + return addWord(a_sz, b_sz); + } +} + + +contract Size { + function main() public returns (word) { + let tupleProxy: Proxy<(word, (word, ()))> = Proxy.Proxy; + return StorageSize.size(tupleProxy); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/StdSize.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/StdSize.solc new file mode 100644 index 00000000..5b02a0e0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/StdSize.solc @@ -0,0 +1,48 @@ +enum Proxy { Proxy } + +function addWord(l: word, r: word) returns (word) { + let rw: word; + assembly { + rw := add(l, r) + } + return rw; +} + +trait StorageSize { + function size(x: Proxy) returns (word) ; +} + +default impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<()> { + function size(x: Proxy<()>) returns (word) { + return 0; + } +} + +impl StorageSize { + function size(x: Proxy) returns (word) { + return 1; + } +} + +impl StorageSize<(a, b)> where a: StorageSize, b: StorageSize { + function size(x: Proxy<(a, b)>) returns (word) { + let aProxy: Proxy = Proxy.Proxy; + let bProxy: Proxy = Proxy.Proxy; + let a_sz: word = StorageSize.size(aProxy); + let b_sz: word = StorageSize.size(bProxy); + return addWord(a_sz, b_sz); + } +} + +contract Size { + function main() public returns (word) { + let tupleProxy: Proxy<(word, (word, ()))> = Proxy.Proxy; + return StorageSize.size(tupleProxy); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/comptime_syntax.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/comptime_syntax.solc new file mode 100644 index 00000000..12a8cbd7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/comptime_syntax.solc @@ -0,0 +1,15 @@ +contract ComptimeSyntax { + + function f(comptime x: word) returns (comptime word) { + return x; + } + + function g() returns (word) { + let comptime y: word = f(42); + return y; + } + + function main() returns (word) { + return g(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/counter.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/counter.solc new file mode 100644 index 00000000..b3e5878a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/counter.solc @@ -0,0 +1,25 @@ +import std; +import {uint256, address} from std; +import std.dispatch; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; +contract Counter { + // some dummy fields to test offset calculation + fld0 : word; + fld1 : uint256; + fld2 : address; + counter : word; + + constructor() { + counter = 41; + fld2 = address.address(0); + fld1 = uint256.uint256(11); + fld0 = 7; + } + + function main() public returns (word) { + counter = counter + 1; + return counter; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_mem.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_mem.solc new file mode 100644 index 00000000..a6351d53 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_mem.solc @@ -0,0 +1,19 @@ +/* Positive: function using mstore+mload in assembly is comptime-evaluable + when its argument is known at compile time. + The evaluator runs in comptime mode for the RHS of `let x : comptime`. +*/ +function storeLoad(x: word) returns (word) { + let r: word; + assembly { + mstore(0, x) + r := mload(0) + } + return r; +} + +contract ComptimeAsmMem { + function main() returns (word) { + let comptime res: word = storeLoad(42); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_ret.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_ret.solc new file mode 100644 index 00000000..83ffee12 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_asm_ret.solc @@ -0,0 +1,17 @@ +/* Negative: function annotated '-> comptime word' but body reads from + storage via sload — storage is mutable state, never comptime. + The verifier must reject this. +*/ + +contract ComptimeAsmRet { + function loadFromStorage() returns (comptime word) { + let v: word; + assembly { + v := sload(0) + } + return v; + } + function main() returns (word) { + return loadFromStorage(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_chain_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_chain_ok.solc new file mode 100644 index 00000000..026cb956 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_chain_ok.solc @@ -0,0 +1,16 @@ +/* Positive: comptime result threaded through two comptime functions. + increment(20) is comptime, so it can be passed to double's comptime param. +*/ +import * as std from std; + +contract ComptimeChainOk { + function increment(comptime x: word) returns (comptime word) { + return x + 1; + } + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + return double(increment(20)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_ok.solc new file mode 100644 index 00000000..4f629de2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_ok.solc @@ -0,0 +1,12 @@ +/* Positive: comptime let binding fed from a comptime function call. */ +import * as std from std; + +contract ComptimeLetOk { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + let comptime y: word = double(21); + return y; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_runtime.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_runtime.solc new file mode 100644 index 00000000..526fcbeb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_let_runtime.solc @@ -0,0 +1,21 @@ +/* Negative: comptime let bound to a runtime expression — must fail. + sloadWord reads from storage (sload); storage is mutable state, + so its result is runtime. Binding it with 'let y : comptime word' + must be rejected by the verifier. +*/ +import * as std from std; + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract ComptimeLetRuntime { + function main() returns (word) { + let comptime y: word = sloadWord(); + return y; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_bad.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_bad.solc new file mode 100644 index 00000000..04c87d32 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_bad.solc @@ -0,0 +1,27 @@ +/* Negative: Scale instance whose 'scale' reads from storage — not comptime. + Despite the comptime annotations on the method signature, the word + instance body uses sload (mutable storage state), making the result + a runtime value. The verifier must reject the comptime let binding. +*/ +import * as std from std; + +trait Scale { + function scale(comptime factor: word, comptime x: a) returns (comptime a) ; +} + +impl Scale { + function scale(comptime factor: word, comptime x: word) returns (comptime word) { + let base: word; + assembly { + base := sload(0) + } + return base + x * factor; + } +} + +contract ComptimeOverloadedBad { + function main() returns (word) { + let comptime a: word = Scale.scale(3, 10); + return a; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_ok.solc new file mode 100644 index 00000000..bd765f1e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_overloaded_ok.solc @@ -0,0 +1,29 @@ +/* Positive: comptime through an overloaded (type class) function. + Scale.scale takes a comptime factor; if factor == 1 it returns x + unchanged (conditional evaluated at comptime since factor is comptime). + mulWord is builtinPure, so multiplication of comptime values is comptime. + The verifier must follow specialization and accept this. +*/ +import std; + +trait Scale { + function scale(comptime factor: word, comptime x: a) returns (comptime a) ; +} + +impl Scale { + function scale(comptime factor: word, comptime x: word) returns (comptime word) { + if (factor == 1) { + return x; + } else { + return x * factor; + } + } +} + +contract ComptimeOverloadedOk { + function main() returns (word) { + let comptime a: word = Scale.scale(1, 32); + let comptime b: word = Scale.scale(3, 10); + return a + b; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_param_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_param_ok.solc new file mode 100644 index 00000000..a1f05515 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_param_ok.solc @@ -0,0 +1,14 @@ +/* Positive: literal passed to comptime param. + x+x desugars to Add.add(x,x) -> addWord(x,x), which is builtinPure, + so the comptime annotation on the result is valid. +*/ +import * as std from std; + +contract ComptimeParamOk { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + return double(21); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_runtime_arg.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_runtime_arg.solc new file mode 100644 index 00000000..c2e85a7e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/ct_runtime_arg.solc @@ -0,0 +1,22 @@ +/* Negative: runtime value passed to a comptime parameter — must fail. + sloadWord uses sload; storage is mutable state, so its result is + a runtime value; passing it to double's comptime param is an error. +*/ +import * as std from std; + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract ComptimeRuntimeArg { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + return double(sloadWord()); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib.solc new file mode 100644 index 00000000..0f2dfd84 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib.solc @@ -0,0 +1,14 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function fib(n: word) returns (word) { + if(n < 2) { return n; } else {return fib(n-1) + fib(n-2); } +} + +contract Fib { +function main() public returns (word) { + return fib(10); +} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib2.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib2.solc new file mode 100644 index 00000000..70789115 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib2.solc @@ -0,0 +1,12 @@ +import std; + +function fib2(n: word) returns (comptime word) { + if(n < 2) { return n; } else {return fib2(n-1) + fib2(n-2); } +} + +contract Fib { + function main() returns (word) { + let comptime res: word = fib2(10); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib3.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib3.solc new file mode 100644 index 00000000..2834655c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/fib3.solc @@ -0,0 +1,12 @@ +import std; + +function fib3(n: word) returns (word) { + if(n < 2) { return n; } else {return fib3(n-1) + fib3(n-2); } +} + +contract Fib { + function main() returns (word) { + let comptime res: word = fib3(10); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/int-untyped-let.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/int-untyped-let.solc new file mode 100644 index 00000000..190eacc5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/int-untyped-let.solc @@ -0,0 +1,20 @@ +// Bare integer literals with integer class instances from std. + +import {Eq, Ord, lt, Add, Sub} from std; + +function fib(comptime n: integer) returns (comptime integer) { + if (n < 2) { + return n; + } else { + return + fib(n - 1) + fib(n - 2); + } +} + +contract IntegerLit { + function main() returns (word) { + let x = 20; + let comptime res: word = Int.fromInteger(fib(x)); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-basic.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-basic.solc new file mode 100644 index 00000000..a7072fc1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-basic.solc @@ -0,0 +1,11 @@ +// Exercises integer primitives: wordToInteger, wordFromInteger, integerAdd, integerMul. +// Integer-typed lets are implicitly comptime; literals are polymorphic via FromInteger. +// Expected: main() folds to word literal 100. + +contract IntegerBasic { + function main() returns (word) { + let x = 42; + let y = integerAdd(x, 8); + return wordFromInteger(integerMul(y, 2)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-fib.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-fib.solc new file mode 100644 index 00000000..ce194c36 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-fib.solc @@ -0,0 +1,20 @@ +// Fibonacci using the comptime-only integer type. +// No import std needed: uses only compiler builtins. +// Expected: main() folds to word literal 55 (fib(10)). + +function fib(comptime n: integer) returns (comptime integer) { + if (integerLt(n, 2)) { + return n; + } else { + return integerAdd( + fib(integerSub(n, 1)), + fib(integerSub(n, 2)) + ); + } +} + +contract FibInteger { + function main() returns (word) { + return wordFromInteger(fib(10)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-from-integer.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-from-integer.solc new file mode 100644 index 00000000..8a7cfa5b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-from-integer.solc @@ -0,0 +1,28 @@ +import std; + +// Tests Num.fromInteger for word (Typedef.abs = identity) and uint256 (wraps in uint256(...)). +// Also tests the full design-doc pattern: comptime integer fib result converted via Num.fromInteger. + +function fib(comptime n: integer) returns (comptime integer) { + if (integerLt(n, wordToInteger(2))) { + return n; + } else { + return integerAdd( + fib(integerSub(n, wordToInteger(1))), + fib(integerSub(n, wordToInteger(2))) + ); + } +} + +// Exercises both instances. +// word path: Typedef.abs for word is identity => fromInteger(wordToInteger(42)) = 42 +// uint256 path: Typedef.abs wraps in uint256 => fromInteger(fib(10)) = uint256(55) +// Returns Typedef.rep(u) = 55, demonstrating the uint256 round-trip. +// Expected: main() folds to word literal 55. +contract IntegerFromInteger { + function main() returns (word) { + let comptime w: word = Num.fromInteger(wordToInteger(42)); + let comptime u: uint256 = Num.fromInteger(fib(wordToInteger(10))); + return Typedef.rep(u); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-class.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-class.solc new file mode 100644 index 00000000..82f4c863 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-class.solc @@ -0,0 +1,22 @@ +// Bare integer literals with integer class instances from std. +// The type checker infers the literal type from context: the integer:Ord/Add/Sub +// instances constrain unresolved literals to `integer`. + +import {Eq, Ord, lt, Add, Sub} from std; + +function fib(comptime n: integer) returns (comptime integer) { + if (n < 2) { + return n; + } else { + return + fib(n - 1) + fib(n - 2); + } +} + +contract IntegerLit { + function main() returns (word) { + let comptime x: integer = 20; + let comptime res: word = wordFromInteger(fib(x)); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-cond.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-cond.solc new file mode 100644 index 00000000..975e8a20 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-cond.solc @@ -0,0 +1,11 @@ +// Integer literals in conditional expression branches. +// The expected type is propagated to both branches of a Cond, so literals +// in branches infer the correct type. + +contract CondLit { + function main() returns (word) { + // Both literal branches should infer type word from the return annotation. + let x: word = ((true) ? 1 : 2); + return x; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-pat.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-pat.solc new file mode 100644 index 00000000..6539bfd6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-pat.solc @@ -0,0 +1,19 @@ +// Integer literal patterns against word and integer scrutinees. + +import {Add} from std; + +function classify_word(comptime n: word) returns (comptime word) { + match (n) { case 0 { return 10; } case 1 { return 20; } default { return 0; } } +} + +function classify_integer(comptime n: integer) returns (comptime integer) { + match (n) { case 0 { return integerAdd(n, 10); } case 1 { return integerAdd(n, 20); } default { return n; } } +} + +contract PatternLit { + function main() returns (word) { + let comptime a: word = classify_word(1); + let comptime b: integer = classify_integer(0); + return Add.add(a, wordFromInteger(b)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-poly.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-poly.solc new file mode 100644 index 00000000..c6e7f250 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-poly.solc @@ -0,0 +1,19 @@ +// Polymorphic literal inference: the type of an unannotated integer literal is +// determined by unification with the surrounding context. +// +// Add.add(s, 1) with s:word => 1 infers as word (Add a => a->a->a, a=word) +// integerAdd(n, 1) with n:integer => 1 infers as integer (param type is integer) + +import {Add} from std; + +contract PolyLit { + function main() returns (word) { + let s: word = 0; + // 1 inferred as word via Add.add constraint + let s2: word = Add.add(s, 1); + // literal in integer context; type and comptime inferred + let n = wordToInteger(s2); + let n2 = integerAdd(n, 1); + return wordFromInteger(n2); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-safe.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-safe.solc new file mode 100644 index 00000000..a6c10669 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-safe.solc @@ -0,0 +1,28 @@ +import std; + +// Safety: verify literals pick up the correct type from context, no spurious coercions. +// +// addWord(1, 2) — word params, so 1 and 2 get wordFromInteger coercions +// wordToInteger(42) — word param, so 42 gets wordFromInteger coercion +// integerEq(wordToInteger(42), wordToInteger(42)) +// — the 42 literals are inside wordToInteger calls (word param) +// let z : word = 5 — explicit word annotation, wordFromInteger coercion inserted + +contract IntegerLitSafe { + function main() returns (word) { + // word arithmetic: 1 and 2 must stay as word literals + let a: word = addWord(1, 2); + + // already-explicit coercions: no double-wrapping of the inner 42 + let comptime ok: bool = integerEq(wordToInteger(42), wordToInteger(42)); + + // wordFromInteger param is integer, but wordToInteger(10) is a Call not a + // literal, so no double-wrap; b folds to 10 + let comptime b: word = wordFromInteger(wordToInteger(10)); + + // word-annotated let: annotation is word, not integer -> no coercion + let z: word = 5; + + return addWord(a, addWord(b, z)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-word-site.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-word-site.solc new file mode 100644 index 00000000..85223f1b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit-word-site.solc @@ -0,0 +1,13 @@ +// Integer literals at word-typed sites receive automatic wordFromInteger coercions. +// Tests: +// let x : word = N -- explicit word annotation +// return N -- return in word-returning function +// passing literal to word parameter + +contract WordSite { + function main() returns (word) { + let a: word = 42; + let b: word = 0; + return a; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit.solc new file mode 100644 index 00000000..b4cd3c2c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/integer-lit.solc @@ -0,0 +1,26 @@ +// Bare integer literals at `integer` sites, without explicit wordToInteger. +// The type checker infers the literal type from the expected type at each site: +// let x : comptime integer = 10 -- expected type is integer +// integerLt(n, 2) -- param type is integer +// integerSub(n, 1) -- param type is integer +// +// Expected: main() folds to word literal 55 (fib(10)). + +function fib(comptime n: integer) returns (comptime integer) { + if (integerLt(n, 2)) { + return n; + } else { + return integerAdd( + fib(integerSub(n, 1)), + fib(integerSub(n, 2)) + ); + } +} + +contract IntegerLit { + function main() returns (word) { + let comptime x: integer = 10; + let comptime res: word = wordFromInteger(fib(x)); + return res; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/match_labels.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/match_labels.solc new file mode 100644 index 00000000..b6c026f2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/match_labels.solc @@ -0,0 +1,19 @@ +/* Test comptime expression match labels: the intended use case is + matching function selectors against keccak hashes of signatures. + Covers: keccakLit of a literal, keccakLit of a concatenation, wildcard. +*/ + +import std; + +contract MatchLabels { + + function dispatch(selector: word) returns (word) { + match (selector) { case comptime keccakLit("transfer(address,uint256)") { return 1; } case comptime keccakLit("balanceOf" + "(" + "address" + ")") { return 2; } default { return 0; } } + } + + function main() returns (word) { + let comptime t: word = keccakLit("transfer(address,uint256)"); + let comptime b: word = keccakLit("balanceOf(address)"); + return dispatch(t) + dispatch(b) + dispatch(0); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-keccak.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-keccak.solc new file mode 100644 index 00000000..395b1cc2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-keccak.solc @@ -0,0 +1,11 @@ +import * as std from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract StringLitKeccak { + function main() public returns (word) { + // keccakLit folds to a 256-bit word (EVM/Yul semantics) + return std.keccakLit("abc"); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-len.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-len.solc new file mode 100644 index 00000000..0c6a9172 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-len.solc @@ -0,0 +1,11 @@ +import * as std from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract StringLitLen { + function main() public returns (word) { + // strlenLit folds to a word + return std.strlenLit("hello"); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-ops.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-ops.solc new file mode 100644 index 00000000..9b7538fe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-ops.solc @@ -0,0 +1,15 @@ +import * as std from std; +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +// These functions are intended to be folded by MastEval at compile time. + +contract StringLitOps { + function main() public returns () { + // concatLit folds to a string literal, enabling revertLit("...") lowering + let comptime s: string = concatLit("ab", "cd"); + std.revertLit(s); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/uint256-lit.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/uint256-lit.solc new file mode 100644 index 00000000..88b4bd8e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/uint256-lit.solc @@ -0,0 +1,13 @@ +// Bare integer literals at uint256-typed sites use `instance uint256 : Int`. +// The instance's fromInteger wraps `wordFromInteger`, so an out-of-range +// literal is truncated mod 2^256, matching the `word` site behaviour. +import std; + +contract Uint256Lit { + function main() returns (word) { + let a: uint256 = 3; + // 2^256 + 5 must truncate to 5. + let b: uint256 = 0x10000000000000000000000000000000000000000000000000000000000000005; + return Typedef.rep(a) + Typedef.rep(b); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/Revert.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/Revert.solc new file mode 100644 index 00000000..88664c94 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/Revert.solc @@ -0,0 +1,19 @@ +import std; +import std.dispatch; + +function my_revert() returns (word) { + revertLit("regression"); + return 0; +} + +contract Foo { + constructor() {} + + function noAnswer() public returns (uint256) { + return uint256.uint256(my_revert()); + } + + function answer() public returns (uint256) { + return uint256.uint256(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/assembly.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/assembly.solc new file mode 100644 index 00000000..88482c9a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/assembly.solc @@ -0,0 +1,19 @@ +import std; +import std.dispatch; + +contract C { + constructor() {} + + // Exercises a Yul block that declares an uninitialized `let y`, assigns the + // boolean literal `true` to it, and writes it back to the surrounding + // `word` local `x`. `true` is the word `1`, so this returns uint256(1). + function asmBool() public returns (uint256) { + let x: word; + assembly { + let y + y := true + x := y + } + return uint256.uint256(x); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/basic.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/basic.solc new file mode 100644 index 00000000..da6e6d61 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/basic.solc @@ -0,0 +1,95 @@ +import std; +import std.dispatch; +import {address as address_} from std.opcodes; + +function self() returns (address) { + return address.address(address_()); +} + +contract C { + constructor() {} + function nothing() public returns () {} + + // Re-enters this very contract via raw_call(address(this), ...). The payload + // is the 4-byte selector of an existing entry point (something(), 0xa7a0d537), + // built by left-aligning it in a bytes32 and truncating to 4 bytes. The inner + // call succeeds, so raw_call reports ok == true and returns its returndata + // (the abi-encoded uint256(1)). + function callSelf() public returns (bool, bytes memory) { + let sel: bytes32 = bytes32.bytes32(0xa7a0d53700000000000000000000000000000000000000000000000000000000); + let payload = truncate(to_bytes(sel), 4); + match (raw_call(self(), uint256.uint256(0), payload)) { case (ok, ret) { return (ok, ret); } } + } + + // Same shape, but the selector (0xdeadc0de) matches no entry point, so dispatch + // reverts (there is no fallback). raw_call swallows the inner revert and reports + // ok == false; this outer call itself still succeeds and returns the revert + // returndata (the 4-byte NoFallback error selector). + function callSelfInvalid() public returns (bool, bytes memory) { + let sel: bytes32 = bytes32.bytes32(0xdeadc0de00000000000000000000000000000000000000000000000000000000); + let payload = truncate(to_bytes(sel), 4); + match (raw_call(self(), uint256.uint256(0), payload)) { case (ok, ret) { return (ok, ret); } } + } + + function something() public returns (uint256) { + return uint256.uint256(1); + } + + function add2(x: uint256, y: uint256) public returns (uint256) { + return Add.add(x,y); + } + + function add3(x: uint256, y: uint256, z: uint256) public returns (uint256) { + return Add.add(z, Add.add(x,y)); + } + + function addmod3(x: uint256, y: uint256, k: uint256) public returns (uint256) { + return addmod(x, y, k); + } + + function mulmod3(x: uint256, y: uint256, k: uint256) public returns (uint256) { + return mulmod(x, y, k); + } + + // Bitwise / modulo via the syntactic sugar only (no explicit class calls): + // `^` -> BitXor.bxor, `|` -> BitOr.bor, `&` -> BitAnd.band, `%` -> Mod.mod. + function bxor2(x: uint256, y: uint256) public returns (uint256) { + return x ^ y; + } + + function bor2(x: uint256, y: uint256) public returns (uint256) { + return x | y; + } + + function band2(x: uint256, y: uint256) public returns (uint256) { + return x & y; + } + + function mod2(x: uint256, y: uint256) public returns (uint256) { + return x % y; + } + + function id_bytes(b: bytes memory) public returns (bytes memory) { + return b; + } + + function id_string(b: string memory) public returns (string memory) { + return b; + } + + function id_bytes32(b: bytes32) public returns (bytes32) { + return b; + } + + function id_address(a: address) public returns (address) { + return a; + } + + function id_pair() public returns (uint256, uint256) { + return (uint256.uint256(7), uint256.uint256(11)); + } + + function hidden() returns (uint256) { + return uint256.uint256(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/concat.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/concat.solc new file mode 100644 index 00000000..d51d17e5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/concat.solc @@ -0,0 +1,42 @@ +import std; +import std.dispatch; + +contract C { + constructor() {} + + function concat_b32_b32(a: bytes32, b: bytes32) public returns (bytes memory) { + return concat(a, b); + } + + function concat_b32_bytes(a: bytes32, b: bytes memory) public returns (bytes memory) { + return concat(a, b); + } + + function concat_bytes_bytes(a: bytes memory, b: bytes memory) public returns (bytes memory) { + return concat(a, b); + } + + function to_bytes_b32(a: bytes32) public returns (bytes memory) { + return to_bytes(a); + } + + function to_bytes_bytes(a: bytes memory) public returns (bytes memory) { + return to_bytes(a); + } + + function empty_area(n: uint256) public returns (bytes memory) { + return to_bytes(empty.empty(Typedef.rep(n))); + } + + function concat_b32_empty(a: bytes32, n: uint256) public returns (bytes memory) { + return concat(a, empty.empty(Typedef.rep(n))); + } + + function concat_nested_b32(a: bytes32, b: bytes32, c: bytes32) public returns (bytes memory) { + return concat(a, concat(b, c)); + } + + function concat_nested_empty(a: bytes32, n: uint256, c: bytes32) public returns (bytes memory) { + return concat(a, concat(empty.empty(Typedef.rep(n)), c)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/counter.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/counter.solc new file mode 100644 index 00000000..e1300112 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/counter.solc @@ -0,0 +1,14 @@ +import std; +import std.dispatch; +contract Counter { + counter : uint256; + + constructor() { + counter = 41; + } + + function test() public returns (uint256) { + counter = counter + 1; + return counter; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ecrecover.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ecrecover.solc new file mode 100644 index 00000000..c9eb84ce --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ecrecover.solc @@ -0,0 +1,24 @@ +import std; +import std.dispatch; + +contract EcrecoverTest { + function recover() public returns (address) { + let h: bytes32 = bytes32.bytes32(0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899); + let v: uint256 = uint256.uint256(27); + let r: bytes32 = bytes32.bytes32(0xb3ba6dd3757d18f28736e84b1296af85362b7bdf4548710733c6325abf95311d); + let s: bytes32 = bytes32.bytes32(0x3523e7d34da277c59af090e44cebddb10b73be11780f028d02cf5ae5f24109fc); + return ecrecover(h, v, r, s); + } + + // r = 0 is an invalid signature component: the precompile succeeds (ret != 0) + // but recovers nothing, so it returns empty output and `res` stays 0. This + // exercises the `ECRecoverFailed()` (0x4fbfae63) revert path. `v` and `s` + // are kept well-formed so neither the malleability nor call-failed guards fire. + function recoverFail() public returns (address) { + let h: bytes32 = bytes32.bytes32(0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899); + let v: uint256 = uint256.uint256(27); + let r: bytes32 = bytes32.bytes32(0x0); + let s: bytes32 = bytes32.bytes32(0x3523e7d34da277c59af090e44cebddb10b73be11780f028d02cf5ae5f24109fc); + return ecrecover(h, v, r, s); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty.solc new file mode 100644 index 00000000..6e23333e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty.solc @@ -0,0 +1,6 @@ +import std; +import std.dispatch; + +contract C { + constructor() {} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty_no_constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty_no_constructor.solc new file mode 100644 index 00000000..b01f003d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/empty_no_constructor.solc @@ -0,0 +1,5 @@ +import std; +import std.dispatch; + +contract C { +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/fallback.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/fallback.solc new file mode 100644 index 00000000..029776ef --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/fallback.solc @@ -0,0 +1,14 @@ +import std; +import std.dispatch; + +contract WithFallback { + constructor() {} + + function answer() public returns (uint256) { + return uint256.uint256(42); + } + + fallback() returns () { + revertLit("fallback-was-called"); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/forloops.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/forloops.solc new file mode 100644 index 00000000..176529b9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/forloops.solc @@ -0,0 +1,95 @@ +import std; +import std.dispatch; + +contract C { + counter : uint256; + + constructor() { + counter = uint256.uint256(0); + } + + function bump() returns (uint256) { + counter = counter + uint256.uint256(1); + return counter; + } + + function getCounter() public returns (uint256) { + return counter; + } + + // Sum of 0..4 with early `break` at i == 5. + function break_sum() public returns (uint256) { + let s: uint256 = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(0); i < uint256.uint256(10); i = i + uint256.uint256(1)) { + if (i == uint256.uint256(5)) { + break; + } else {} + s = s + i; + } + return s; + } + + // Sum of 5..9 using `continue` to skip the iterations where i < 5. + // The post-statement (i = i + 1) must still run on `continue`, otherwise + // the loop would never terminate. + function continue_sum() public returns (uint256) { + let s: uint256 = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(0); i < uint256.uint256(10); i = i + uint256.uint256(1)) { + if (i < uint256.uint256(5)) { + continue; + } else {} + s = s + i; + } + return s; + } + + // Empty initializer: `i` is declared/initialised outside the loop. + function empty_init() public returns (uint256) { + let i: uint256 = uint256.uint256(3); + let s: uint256 = uint256.uint256(0); + for (; i < uint256.uint256(7); i = i + uint256.uint256(1)) { + s = s + i; + } + return s; + } + + // Empty post-body: the increment is done in the loop body. + function empty_post() public returns (uint256) { + let s: uint256 = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(0); i < uint256.uint256(4); ) { + s = s + i; + i = i + uint256.uint256(1); + } + return s; + } + + // Side effect in the condition: `bump()` increments storage on every + // probe (including the failing one), so observing `counter` afterwards + // proves the condition ran the expected number of times. + function cond_side_effect() public returns (uint256) { + counter = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(0); bump() < uint256.uint256(5); i = i + uint256.uint256(1)) {} + return counter; + } + + // Side effect in the post-body: `bump()` runs once per completed + // iteration, so `counter` ends equal to the iteration count. + function post_side_effect() public returns (uint256) { + counter = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(0); i < uint256.uint256(3); bump()) { + i = i + uint256.uint256(1); + } + return counter; + } + + // Nested `for` -- sum of i*j for i,j in 1..3. + function double_loop() public returns (uint256) { + let s: uint256 = uint256.uint256(0); + for (let i: uint256 = uint256.uint256(1); i < uint256.uint256(4); i = i + uint256.uint256(1)) { + for (let j: uint256 = uint256.uint256(1); j < uint256.uint256(4); j = j + uint256.uint256(1)) { + s = s + i * j; + } + } + return s; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_product.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_product.solc new file mode 100644 index 00000000..7de06576 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_product.solc @@ -0,0 +1,51 @@ +import std; +import std.dispatch; +import {mload, mstore} from std.opcodes; +import std.Generic; +import std.ABIGeneric; + +pragma solcore noGenericInstanceFor Point; + +enum Point { Point(uint256, uint256) } + +// Only requirement: Generic instance using the primitive pair type. +// rep = (uint256, uint256) — primitive Solcore pair +impl Generic { + function from(p: Point) returns (uint256, uint256) { + match (p) { case Point.Point(x, y) { return (x, y); } } + } + function to(t: (uint256, uint256)) returns (Point) { + match (t) { case (x, y) { return Point.Point(x, y); } } + } +} + +contract GenericProduct { + constructor() {} + + // Calls encode; returns word at offset 0 (the x field). + function encodeX(a: uint256, b: uint256) public returns (uint256) { + let p: Point = Point.Point(a, b); + let buf = allocate_zeroed_memory(64); + encode(p, buf, 0, 64); + return Typedef.abs(mload(buf)); + } + + // Calls encode; returns word at offset 32 (the y field). + function encodeY(a: uint256, b: uint256) public returns (uint256) { + let p: Point = Point.Point(a, b); + let buf = allocate_zeroed_memory(64); + encode(p, buf, 0, 64); + return Typedef.abs(mload(buf + 32)); + } + + // Writes [a][b] into memory, calls decode, returns the x field. + function decodeX(a: uint256, b: uint256) public returns (uint256) { + let buf = allocate_zeroed_memory(64); + mstore(buf, Typedef.rep(a)); + mstore(buf + 32, Typedef.rep(b)); + let rdr: MemoryWordReader = MemoryWordReader.MemoryWordReader(buf); + let dec: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + let p: Point = decode(dec, 0); + match (p) { case Point.Point(x, _) { return x; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_sum.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_sum.solc new file mode 100644 index 00000000..44e5982f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/generic_sum.solc @@ -0,0 +1,61 @@ +import std; +import std.dispatch; +import {mload, mstore} from std.opcodes; +import std.Generic; +import std.ABIGeneric; + +pragma solcore noGenericInstanceFor Option; + +enum Option { None, Some(a) } + +// Only requirement: Generic instance using the primitive sum type. +// rep = sum((), uint256): inl(()) = None, inr(v) = Some(v) +impl Generic, sum<(), uint256>> { + function from(x: Option) returns (sum<(), uint256>) { + match (x) { case Option.None { return inl(()); } case Option.Some(v) { return inr(v); } } + } + function to(r: sum<(), uint256>) returns (Option) { + match (r) { case inl(_) { return Option.None; } case inr(v) { return Option.Some(v); } } + } +} + +contract GenericSum { + constructor() {} + + // Calls encode; returns the tag word (first 32 bytes). + // None → 0 + function encodeNone() public returns (uint256) { + let x: Option = Option.None; + let buf = allocate_zeroed_memory(64); + encode(x, buf, 0, 64); + return Typedef.abs(mload(buf)); + } + + // Calls encode; returns the tag word (first 32 bytes). + // Some(n) → 1 + function encodeSomeTag(n: uint256) public returns (uint256) { + let x: Option = Option.Some(n); + let buf = allocate_zeroed_memory(64); + encode(x, buf, 0, 64); + return Typedef.abs(mload(buf)); + } + + // Calls encode; returns the payload word (bytes 32-63). + function encodePayload(n: uint256) public returns (uint256) { + let x: Option = Option.Some(n); + let buf = allocate_zeroed_memory(64); + encode(x, buf, 0, 64); + return Typedef.abs(mload(buf + 32)); + } + + // Writes [tag][value] into memory, calls decode, returns the value or 0. + function decodeAndGet(tag: uint256, value: uint256) public returns (uint256) { + let buf = allocate_zeroed_memory(64); + mstore(buf, Typedef.rep(tag)); + mstore(buf + 32, Typedef.rep(value)); + let rdr: MemoryWordReader = MemoryWordReader.MemoryWordReader(buf); + let dec: ABIDecoder, MemoryWordReader> = ABIDecoder.ABIDecoder(rdr); + let opt: Option = decode(dec, 0); + match (opt) { case Option.None { return uint256.uint256(0); } case Option.Some(v) { return v; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/hashes.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/hashes.solc new file mode 100644 index 00000000..e91a5081 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/hashes.solc @@ -0,0 +1,31 @@ +import std; +import std.dispatch; +import {mstore} from std.opcodes; + +// Build a memory(bytes) holding the three-byte string "abc". +function abcBytes() returns (bytes memory) { + let p = allocate_memory(64); + mstore(p, 3); + mstore(p + 32, 0x6162630000000000000000000000000000000000000000000000000000000000); + return memory.memory(p); +} + +contract C { + constructor() {} + + function keccak() public returns (bytes32) { + return keccak256_(abcBytes()); + } + + function sha() public returns (bytes32) { + return sha256(abcBytes()); + } + + function ripemd() public returns (bytes32) { + return ripemd160(abcBytes()); + } + + function erc7201_(id: bytes memory) public returns (bytes32) { + return erc7201(id); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/memory.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/memory.solc new file mode 100644 index 00000000..c1874059 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/memory.solc @@ -0,0 +1,19 @@ +import std; +import std.dispatch; +import {mstore} from std.opcodes; + +contract C { + function dirty_allocate() public returns (bytes memory) { + mstore(get_free_memory() + 32, 0xdeadc0de); + let ptr = allocate_memory(32 + 32); + mstore(ptr, 32); + return memory.memory(ptr); + } + + function clear_allocate() public returns (bytes memory) { + mstore(get_free_memory() + 32, 0xdeadc0de); + let ptr = allocate_zeroed_memory(32 + 32); + mstore(ptr, 32); + return memory.memory(ptr); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/miniERC20.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/miniERC20.solc new file mode 100644 index 00000000..86283dcb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/miniERC20.solc @@ -0,0 +1,98 @@ +import std; +import std.dispatch; + +function caller() returns (address) { + let res: word; + assembly { + res := caller() + } + return address.address(res); +} + +contract MiniERC20 { + name : string; + symbol : string; + owner : address; + decimals : uint256; // should be uint8 when we get to it + totalSupply : uint256; + balances : mapping(address => uint256); + allowance : mapping(address => mapping(address => uint256)); + + constructor(name_: string memory, symbol_: string memory, totalSupply_: uint256) { + name = name_; + symbol = symbol_; + owner = caller(); + decimals = 18; + mint(totalSupply_); + } + + function name() public returns (string memory) { + return name; + } + + function symbol() public returns (string memory) { + return symbol; + } + + function decimals() public returns (uint256) { + return decimals; + } + + function allowance(owner_: address, spender: address) public returns (uint256) { + return allowance[owner_][spender]; // don't use "owner" here + } + + function balanceOf(account: address) public returns (uint256) { + return balances[account]; + } + + function totalSupply() public returns (uint256) { + return totalSupply; + } + + // Note that this is not access guarded — the minting always goes to the owner + function mint(amount: uint256) public returns () { + balances[owner] = Num.add(balances[owner], amount); + totalSupply = Num.add(totalSupply, amount); + } + + function transfer(dst: address, amt: uint256) public returns (bool) { + return transferFrom(caller(), dst, amt); + } + + function transferFrom(src: address, dst: address, amt: uint256) public returns (bool) { + let msg_sender = caller(); + require(balances[src] >= amt, Error.Error(0xf4d678b8)); // InsufficientBalance() + let maxAllowance: uint256 = Num.maxVal(); + + if (src != msg_sender && allowance[src][msg_sender] != maxAllowance) { + require(allowance[src][msg_sender] >= amt, Error.Error(0x13be252b)); // InsufficientAllowance() + + allowance[src][msg_sender] -= amt; + } + balances[src] = balances[src] - amt; + balances[dst] = balances[dst] + amt; + // emit Transfer(src, dst, amt); + return true; + } + + function approve(usr: address, amt: uint256) public returns (bool) { + let msg_sender = caller(); + allowance[msg_sender][usr] = amt; + // emit Approval(msg.sender, usr, amt); + return true; + } + + + // testing + function getMyBalance() public returns (uint256) { + return balances[caller()]; + } + + function test() public returns (uint256) { + approve(address.address(0), 10); + transferFrom(caller(), address.address(0), 958); + return getMyBalance(); + } + +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/neg.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/neg.solc new file mode 100644 index 00000000..d127d059 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/neg.solc @@ -0,0 +1,54 @@ +import std; +import std.dispatch; + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } +enum Pair { Pair(a, b) } + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + + +function pairfst(p: Pair) returns (a) { + match (p) { case Pair.Pair(x, y) { return x; } } +} + +function pairsnd(p: Pair) returns (b) { + match (p) { case Pair.Pair(x, y) { return y; } } +} + + +impl Neg> where a: Neg, b: Neg { + function neg(p: Pair) returns (Pair) { + return Pair.Pair(Neg.neg (pairfst(p)), Neg.neg(pairsnd(p))); + } +} + +/* +instance (a:Neg,b:Neg) => Pair(a,b):Neg { + function neg(p) { + match p { + | Pair(a,b) => return Pair(neg(a), neg(b)); + } + } +} +*/ + + function bnot(x: B) returns (B) { + match (x) { case B.T { return B.F; } case B.F { return B.T; } } +} + + function fromB(b: B) returns (word) { + match (b) { case B.F { return 0; } case B.T { return 1; } } +} + +contract NegPair { + constructor() {} + function negPair() public returns (uint256) { return uint256.uint256(fromB(pairfst(Neg.neg(Pair.Pair(B.F,B.T))))); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/nonpayable_ctor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/nonpayable_ctor.solc new file mode 100644 index 00000000..4d3dea79 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/nonpayable_ctor.solc @@ -0,0 +1,17 @@ +import std; +import std.dispatch; + +// A contract whose constructor is NOT marked `payable`. Deploying it with an +// incoming value transfer must revert with the NonPayableReceivedValue error +// (selector 0xb5988ea3), exactly like calling a non-payable method with value. +contract NonPayableCtor { + constructor() {} + + function balance() public returns (uint256) { + let value; + assembly { + value := selfbalance() + } + return uint256.uint256(value); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ownable.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ownable.solc new file mode 100644 index 00000000..bdda78f2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/ownable.solc @@ -0,0 +1,31 @@ +import std; +import std.dispatch; + +// caller() is not in the std library yet, +// so every contract must define its own + +function caller() returns (address) { + let res: word; + assembly { + res := caller() + } + return address.address(res); +} + +contract Ownable { + owner : address; + + constructor() { + owner = caller(); + } + + // named getOwner() instead of owner() to avoid collision with the field name + function getOwner() public returns (address) { + return owner; + } + + function changeOwner(newOwner: address) public returns () { + require(caller() == owner, Error.Error(0x12b0c500)); // OwnableUnauthorizedAccount() + owner = newOwner; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable.solc new file mode 100644 index 00000000..381a4f95 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable.solc @@ -0,0 +1,32 @@ +import std; +import std.dispatch; + +contract PayableTest { + constructor() {} + + function deposit() public payable returns (uint256) { + let value; + assembly { + value := callvalue() + } + return uint256.uint256(value); + } + + function balance() public returns (uint256) { + let value; + assembly { + value := selfbalance() + } + return uint256.uint256(value); + } + + fallback() payable returns () { + let value; + assembly { + value := callvalue() + } + if (value == 0) { + revertLit("fallback-was-called-no-value"); + } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable_ctor.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable_ctor.solc new file mode 100644 index 00000000..f6abc5af --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/payable_ctor.solc @@ -0,0 +1,17 @@ +import std; +import std.dispatch; + +// A contract whose constructor is explicitly marked `payable`. +// Deploying it with an incoming value transfer must succeed and the +// transferred value is retained by the newly created contract. +contract PayableCtor { + constructor() payable {} + + function balance() public returns (uint256) { + let value; + assembly { + value := selfbalance() + } + return uint256.uint256(value); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/slices.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/slices.solc new file mode 100644 index 00000000..4aa67b25 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/slices.solc @@ -0,0 +1,65 @@ +import std; +import std.dispatch; + +// Exercises slice_/truncate (memory_slice) composed with concat, to_bytes, +// and the hashing precompiles (keccak256_, sha256). memory_slice implements +// MemorySize + MemoryPointer + MemoryEncode, so it is both sliceable again and +// a valid operand for concat/to_bytes/keccak256_/sha256 with zero copies. +contract C { + // --- slice_/truncate on a memory(bytes), materialized with to_bytes --- + + function slice_bytes(a: bytes memory, start: uint256) public returns (bytes memory) { + return to_bytes(slice_(a, Typedef.rep(start))); + } + + function truncate_bytes(a: bytes memory, end: uint256) public returns (bytes memory) { + return to_bytes(truncate(a, Typedef.rep(end))); + } + + // --- slice_/truncate over the result of a concat --- + + function slice_of_concat(a: bytes32, b: bytes32, start: uint256) public returns (bytes memory) { + return to_bytes(slice_(concat(a, b), Typedef.rep(start))); + } + + function truncate_of_concat(a: bytes32, b: bytes32, end: uint256) public returns (bytes memory) { + return to_bytes(truncate(concat(a, b), Typedef.rep(end))); + } + + // to_bytes(truncate(slice_(concat(a, b), start), end)) -- the headline nesting: + // drop `start` bytes, then keep `end` of what remains (re-slicing a memory_slice). + function window_of_concat(a: bytes32, b: bytes32, start: uint256, end: uint256) public returns (bytes memory) { + return to_bytes(truncate(slice_(concat(a, b), Typedef.rep(start)), Typedef.rep(end))); + } + + // --- a slice used as a concat operand --- + + function concat_slice_b32(a: bytes memory, start: uint256, c: bytes32) public returns (bytes memory) { + return concat(slice_(a, Typedef.rep(start)), c); + } + + function concat_two_slices(a: bytes memory, sa: uint256, b: bytes memory, eb: uint256) public returns (bytes memory) { + return concat(slice_(a, Typedef.rep(sa)), truncate(b, Typedef.rep(eb))); + } + + // --- re-slicing a memory_slice --- + + function slice_of_slice(a: bytes memory, s1: uint256, s2: uint256) public returns (bytes memory) { + return to_bytes(slice_(slice_(a, Typedef.rep(s1)), Typedef.rep(s2))); + } + + // --- hashing a slice directly (no intermediate copy) --- + + function keccak_slice(a: bytes memory, start: uint256) public returns (bytes32) { + return keccak256_(slice_(a, Typedef.rep(start))); + } + + function sha_truncate(a: bytes memory, end: uint256) public returns (bytes32) { + return sha256(truncate(a, Typedef.rep(end))); + } + + // keccak256_(truncate(slice_(concat(a, b), start), end)) -- nested chain, hash endpoint. + function keccak_window_concat(a: bytes32, b: bytes32, start: uint256, end: uint256) public returns (bytes32) { + return keccak256_(truncate(slice_(concat(a, b), Typedef.rep(start)), Typedef.rep(end))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/specialise_sum_of_product.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/specialise_sum_of_product.solc new file mode 100644 index 00000000..93d171de --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/specialise_sum_of_product.solc @@ -0,0 +1,67 @@ +// Regression test: specializer sum-of-product bug (specMatch substitution leak). +// +// A binary class method over the primitive `sum(f, g)` whose two sides have +// DIFFERENT shapes: the inl side carries a product (word, word), the inr side +// carries a plain word. Specializing the instance at sum((word, word), word) +// used to leak a substitution binding from one match alternative into the +// sibling alternative's nested `match`, mistyping its scrutinee. The frontend +// (sol-core) accepted the program, but `yule` then rejected the emitted .hull: +// +// Type mismatch +// expected: sum(word, word) +// actual: sum(pair(word, word), word) +// +// Root cause: in Specialise.hs, `specMatch` did not scope `spSubst` (a global +// accumulator) across match alternatives. While specializing the `inl` branch, +// a binding leaked into the `inr` branch's nested `match`, collapsing +// sum(f, g) to sum(g, g). The fix resets spSubst around each alternative. +// +// This isolates the SPECIALIZER: no #[derive], no Eq universe instances. The +// class and its instances are defined locally and exercised directly, so the +// program must now lower end-to-end and return the expected value. + +import std; +import std.dispatch; + +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +// total(x, y) sums every leaf word of both arguments. +trait Total { + function total(x: a, y: a) returns (word) ; +} + +impl Total { + function total(x: word, y: word) returns (word) { + return x + y; + } +} + +// product: recurse into both components (this is the shape inl carries). +impl Total<(f, g)> where f: Total, g: Total { + function total(x: (f, g), y: (f, g)) returns (word) { + match (x) { case (xa, xb) { match (y) { case (ya, yb) { return Total.total(xa, ya) + Total.total(xb, yb); } } } } + } +} + +// sum: the buggy shape. The inl branch recurses at f (a product here), the inr +// branch recurses at g (a word here); specializing one must not pollute the +// other's nested `match y`. +impl Total> where f: Total, g: Total { + function total(x: sum, y: sum) returns (word) { + match (x) { case inl(xa) { match (y) { case inl(ya) { return Total.total(xa, ya); } case inr(yb) { return 0; } } } case inr(xb) { match (y) { case inl(ya) { return 0; } case inr(yb) { return Total.total(xb, yb); } } } } + } +} + +contract SpecialiseSumOfProduct { + constructor() {} + + // inl carries a product (word, word); the two sum sides differ in shape + // (pair vs word), which is what the specializer mishandled. + // total(inl((1,2)), inl((1,2))) = total((1,2),(1,2)) = (1+1)+(2+2) = 6. + function probe() public returns (uint256) { + let x: sum<(word, word), word> = inl((1, 2)); + let y: sum<(word, word), word> = inl((1, 2)); + return uint256.uint256(Total.total(x, y)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/storage.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/storage.solc new file mode 100644 index 00000000..976b0935 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/storage.solc @@ -0,0 +1,17 @@ +import std; +import std.dispatch; + +// Storage support for a `memory(bytes)` contract field: assigning to the +// field copies the byte array into storage, reading it back loads it into +// fresh memory. Exercises StorageSize / CanStore for memory(bytes). +contract C { + content: bytes; + + function set(value: bytes memory) public returns () { + content = value; + } + + function get() public returns (bytes memory) { + return content; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/stringid.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/stringid.solc new file mode 100644 index 00000000..969b84a8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/stringid.solc @@ -0,0 +1,43 @@ +import std; +import std.dispatch; +import {mstore, mload} from std.opcodes; + +contract C { + constructor() {} + function id(x: string memory) public returns (string memory) { + let ptr: word = Typedef.rep(x); + let len: word; + let n1: word; + assembly { + len := mload(ptr) + n1 := mload(add(ptr,32)) + } + log1(len, 0xc001); + log1(n1, 0xc002); + + return x; + } + + function const_a() public returns (string memory) { + let resPtr = allocate_memory(64); + let payload: word = 0x7777777777777777777777777777777777777777777777777777777777777777; + mstore(resPtr, 3); + mstore(resPtr+32, payload); + return memory.memory(resPtr); + } + function mylen(x: string memory) public returns (uint256) { + let ptr: word = Typedef.rep(x); + let l: word; + let n1: word; + assembly { + l := mload(ptr) + n1 := mload(add(ptr,32)) + } + // log1(l, 0xc001); + // log1(n1, 0xc002); + + return uint256.uint256(l); + } + + // function answer() -> uint256 { return uint256(17); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/sum_wide_product.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/sum_wide_product.solc new file mode 100644 index 00000000..c608af52 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/sum_wide_product.solc @@ -0,0 +1,26 @@ +import std; +import std.dispatch; + +// Regression test for a yule backend bug, independent of the storage/Generic +// work: matching a sum constructor whose payload is a product of arity >= 3. +// +// On `match`, the scrutinee's location is flattened, and the constructor payload +// used to be bound as a flat slot sequence. Destructuring the inner product then +// did EFst on a >2-element sequence and crashed yule with "EFst: type mismatch". +// (A 2-field payload happened to work, since a flat 2-seq is a valid pair.) +// +// No storage and no Generic derivation involved — just constructing and matching +// an ordinary algebraic data type. + +enum Shape { Dot, Tri(uint256, uint256, uint256) } + +contract C { + constructor() {} + + // Build Tri(a,b,c) then match it back out: exercises a sum whose payload is + // a 3-field product. + function triSum(a: uint256, b: uint256, c: uint256) public returns (uint256) { + let s: Shape = Shape.Tri(a, b, c); + match (s) { case Shape.Dot { return uint256.uint256(0); } case Shape.Tri(x, y, z) { return x + y + z; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/weth9.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/weth9.solc new file mode 100644 index 00000000..520da1b9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/weth9.solc @@ -0,0 +1,85 @@ +import std; +import {caller as caller_, callvalue as callvalue_, selfbalance, gas, call} from std.opcodes; +import std.dispatch; + +// Forward `wad` wei to `dst` via a zero-data CALL and revert on failure. +function sendValue(dst: address, wad: uint256) returns () { + let ret = call(gas(), Typedef.rep(dst), Typedef.rep(wad), 0, 0, 0, 0); + require(ret != 0, Error.Error(0x90b8ec18)); // TransferFailed() +} + +function caller() returns (address) { + return address.address(caller_()); +} + +function callvalue() returns (uint256) { + return uint256.uint256(callvalue_()); +} + +// Based on https://github.com/gnosis/canonical-weth/blob/master/contracts/WETH9.sol +// That code is written WITHOUT checked arithmetic. +contract WETH9 { + balances : mapping(address => uint256); + allowance : mapping(address => mapping(address => uint256)); + + constructor() {} + + // --- ETH <-> WETH --- + + function deposit() public payable returns () { + let sender = caller(); + balances[sender] = balances[sender] + callvalue(); + } + + function withdraw(wad: uint256) public returns () { + let sender = caller(); + require(balances[sender] >= wad, Error.Error(0xf4d678b8)); // InsufficientBalance() + balances[sender] = balances[sender] - wad; + sendValue(sender, wad); + } + + // totalSupply == ETH held by this contract (matches canonical WETH9). + function totalSupply() public returns (uint256) { + return uint256.uint256(selfbalance()); + } + + // --- ERC20 surface --- + + function balanceOf(account: address) public returns (uint256) { + return balances[account]; + } + + function allowance(owner_: address, spender: address) public returns (uint256) { + return allowance[owner_][spender]; + } + + function approve(usr: address, wad: uint256) public returns (bool) { + let sender = caller(); + allowance[sender][usr] = wad; + return true; + } + + function transfer(dst: address, wad: uint256) public returns (bool) { + return transferFrom(caller(), dst, wad); + } + + function transferFrom(src: address, dst: address, wad: uint256) public returns (bool) { + let sender = caller(); + let unlimited: uint256 = maxVal(); + require(balances[src] >= wad, Error.Error(0xf4d678b8)); // InsufficientBalance() + + if (src != sender && allowance[src][sender] != unlimited) { + require(allowance[src][sender] >= wad, Error.Error(0x13be252b)); // InsufficientAllowance() + allowance[src][sender] -= wad; + } + balances[src] = balances[src] - wad; + balances[dst] = balances[dst] + wad; + return true; + } + + // Plain ETH transfers (no calldata, just value) auto-wrap into WETH. + fallback() payable returns () { + let sender = caller(); + balances[sender] = balances[sender] + callvalue(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/opcodes/all-shapes.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/opcodes/all-shapes.solc new file mode 100644 index 00000000..61b88c17 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/opcodes/all-shapes.solc @@ -0,0 +1,31 @@ +import std.opcodes; + +// Compilation test for the std/opcodes wrappers. +// Picks two opcodes from each of the four shape categories so the +// pipeline exercises every wrapper signature. + +// no inputs, no return +function shape_void_void() returns () { + stop(); + invalid(); +} + +// no inputs, returns a word +function shape_void_word() returns (word) { + let a = address(); + let t = timestamp(); + return a; +} + +// inputs, no return +function shape_word_void(x: word) returns () { + pop(x); + mstore(0, x); +} + +// inputs, returns a word +function shape_word_word(a: word, b: word) returns (word) { + let s = add(a, b); + let m = mload(0); + return s; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/coverage.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/coverage.solc new file mode 100644 index 00000000..f30c92f2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/coverage.solc @@ -0,0 +1,8 @@ +pragma solcore noCoverageCondition; + +enum List { Nil, Cons(a, List) } +enum Bool { True, False } + +trait C {} + +impl C, a, List> {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/patterson.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/patterson.solc new file mode 100644 index 00000000..0d59a7a0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/pragmas/patterson.solc @@ -0,0 +1,16 @@ + +trait A {} +trait B {} +trait C {} +trait D {} + + +enum Uint256 { U } +enum T { T } +enum S { SCons } + +// This works. +impl D> where U: A {} + +// This should also work, but reports a violation of the Paterson condition. +impl D> where U: A, U: B, U: C {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/00answer.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/00answer.solc new file mode 100644 index 00000000..48c89978 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/00answer.solc @@ -0,0 +1,5 @@ +contract Answer { + function main() public returns (word) { + return 42; + } +} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/01id.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/01id.solc new file mode 100644 index 00000000..9ab26b43 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/01id.solc @@ -0,0 +1,14 @@ +contract Id1 { + + enum Bool { False, True } + + function id(x: word) public returns (word) { + return x ; + } + + function const(x: word, y: Bool) public returns (word) { return x; } + + function main() public returns (word) { + return const(id(42), Bool.False); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/021not.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/021not.solc new file mode 100644 index 00000000..e8e3c4fe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/021not.solc @@ -0,0 +1,15 @@ +contract Not { + enum Bool { False, True } + + function main() public returns (word) { + return fromBool(bnot(Bool.False)); + } + + function fromBool(b: Bool) public returns (word) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } + } + + function bnot(b: Bool) public returns (Bool) { + match (b) { case Bool.False { return Bool.True; } case Bool.True { return Bool.False; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/022add.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/022add.solc new file mode 100644 index 00000000..85258483 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/022add.solc @@ -0,0 +1,13 @@ +function add(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +contract Add1 { + function main() public returns (word) { + return add(40, 2); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/024arith.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/024arith.solc new file mode 100644 index 00000000..4043007d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/024arith.solc @@ -0,0 +1,64 @@ + + +function add(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + +function sub(x: word, y: word) returns (word) { + let res: word; + assembly { + res := sub(x, y) + } + return res; +} + +function div(x: word, y: word) returns (word) { + let res: word; + assembly { + res := div(x, y) + } + return res; +} + +function sdiv(x: word, y: word) returns (word) { + let res: word; + assembly { + res := sdiv(x, y) + } + return res; +} + +function mod(x: word, y: word) returns (word) { + let res: word; + assembly { + res := mod(x, y) + } + return res; +} + +function smod(x: word, y: word) returns (word) { + let res: word; + assembly { + res := smod(x, y) + } + return res; +} + +function exp(x: word, y: word) returns (word) { + let res: word; + assembly { + res := exp(x, y) + } + return res; +} + + +contract Arith { + function main() public returns (word) { + return add(mod(sub(div(exp(2,18),4), 1), 16), 27); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/02nid.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/02nid.solc new file mode 100644 index 00000000..320943c0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/02nid.solc @@ -0,0 +1,15 @@ +contract Id1 { + function id(x: word) public returns (word) { + return x ; + } + + function nid(x: word) public returns (word) { + return id(x); + } + + function const(x: word, y: word) public returns (word) { return x; } + + function main() public returns (word) { + return const(nid(42), id(1)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/031maybe.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/031maybe.solc new file mode 100644 index 00000000..b9214087 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/031maybe.solc @@ -0,0 +1,13 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function main() public returns (word) { + return maybe(0, Option.Some(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/032simplejoin.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/032simplejoin.solc new file mode 100644 index 00000000..8c1508c4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/032simplejoin.solc @@ -0,0 +1,22 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + + function join(mmx: Option>) public returns (Option) { + match (mmx) { case Option.None { return Option.None; } case Option.Some(Option.None) { return Option.None; } case Option.Some(Option.Some(x)) { return Option.Some(x); } } + } + + function join2(mmx: Option>) public returns (Option) { + match (mmx) { case Option.Some(m) { match (m) { case Option.None { return Option.None; } case Option.Some(x) { return Option.Some(x); } } } default { return Option.None; } } + } + + function main() public returns (word) { + return maybe(0, join(Option.Some(Option.Some(42)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/033join.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/033join.solc new file mode 100644 index 00000000..ae046781 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/033join.solc @@ -0,0 +1,17 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + match (mmx) { case Option.Some(Option.Some(x)) { return Option.Some(x); } default { return Option.None; } } + } + + function main() public returns (word) { + return maybe(0, join(Option.Some(Option.Some(42)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/034cojoin.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/034cojoin.solc new file mode 100644 index 00000000..2b3ca81d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/034cojoin.solc @@ -0,0 +1,30 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + let result = Option.None; + match (mmx) { case Option.Some(Option.Some(x)) { result = Option.Some(x); } case Option.None { result = Option.None; } case Option.Some(Option.None) { result = Option.None; } default { result = Option.None; } } + return result; + } + + function extract(mx: Option) public returns (word) { + match (mx) { case Option.Some(x) { return x; } case Option.None { return 0; } } + } + + function cojoin(x: Option) public returns (Option>) { // Test that sum types can grow + let result = Option.None; + result = Option.Some(x); + return result; + } + + + function main() public returns (word) { + return maybe(0, join(cojoin(Option.Some(42)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/035padding.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/035padding.solc new file mode 100644 index 00000000..6d154a1c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/035padding.solc @@ -0,0 +1,11 @@ +contract Option { + enum Option { None, Some(a) } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.Some(x) { return x; } case Option.None { return n; } } + } + + function main() public returns (word) { + return maybe(7, Option.None); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/036wildcard.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/036wildcard.solc new file mode 100644 index 00000000..f1eced27 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/036wildcard.solc @@ -0,0 +1,11 @@ +contract Option { + enum Option { None, Some(a) } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.Some(x) { return x; } default { return n; } } + } + + function main() public returns (word) { + return maybe(7, Option.None); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/037dwarves.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/037dwarves.solc new file mode 100644 index 00000000..dbb9d2df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/037dwarves.solc @@ -0,0 +1,10 @@ +contract Dwarves { + enum Dwarf { Doc, Grumpy, Sleepy, Bashful, Happy, Sneezy, Dopey } + + + function fromEnum(c: Dwarf) public returns (word) { + match (c) { case Dwarf.Doc { return 1; } case Dwarf.Grumpy { return 2; } case Dwarf.Sleepy { return 3; } case Dwarf.Bashful { return 4; } case Dwarf.Happy { return 5; } default { return 0; } } + } + + function main() public returns (word) { return fromEnum(Dwarf.Happy); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/038food0.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/038food0.solc new file mode 100644 index 00000000..aefe54f5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/038food0.solc @@ -0,0 +1,19 @@ +enum Food { Curry, Beans, Other } +enum CFood { Red(Food), Green(Food), Nocolor } + + + + function fromEnum(x: CFood) returns (word) { + match (x) { case CFood.Red(Food.Curry) { return 1; } case CFood.Green(Food.Beans) { return 42; } default { return 3; } } + } + + +contract FoodContract { + function id(x: CFood) public returns (CFood) { + return(x); + } + + function main() public returns (word) { + return fromEnum(id(CFood.Green(Food.Beans))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/039food.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/039food.solc new file mode 100644 index 00000000..c64943a9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/039food.solc @@ -0,0 +1,21 @@ + +enum Food { Curry, Beans, Other } +enum CFood { Red(Food), Green(Food), Nocolor } + + + + + function fromEnum(x: Food) returns (word) { + match (x) { case Food.Curry { return 1; } case Food.Beans { return 42; } case Food.Other { return 3; } } + } + + +contract FoodContract { + function eat(x: CFood) public returns (Food) { + match (x) { case CFood.Red(f) { return f; } case CFood.Green(f) { return f; } default { return Food.Other; } } + } + + function main() public returns (word) { + return fromEnum(eat(CFood.Green(Food.Beans))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/041pair.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/041pair.solc new file mode 100644 index 00000000..b2581f91 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/041pair.solc @@ -0,0 +1,10 @@ +contract Pair { + + function fst(p: (word, word)) public returns (word) { + match (p) { case (a, b) { return a; } } + } + + function main() public returns (word) { + return fst((1,0)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/042triple.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/042triple.solc new file mode 100644 index 00000000..940242b8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/042triple.solc @@ -0,0 +1,10 @@ +contract Triple { + + function asel(t: (word, word, word)) public returns (word) { + match (t) { case (a, b, c) { return c; } } + } + + function main() public returns (word) { + return asel((1,21,42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/043fstsnd.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/043fstsnd.solc new file mode 100644 index 00000000..02e3ce16 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/043fstsnd.solc @@ -0,0 +1,29 @@ + + +enum B { F, T } +enum Pair { Pair(a, b) } + +function fst(p: Pair) returns (a) { + match (p) { case Pair.Pair(x, y) { return x; } } +} + +function snd(p: Pair) returns (b) { + match (p) { case Pair.Pair(x, y) { return y; } } +} + +function add(x: word, y: word) returns (word) { + let res: word; + assembly { + res := add(x, y) + } + return res; +} + + +function addPair(p: Pair) returns (word) { + return add(fst(p), snd(p)); +} + +contract FstSnd { + function main() public returns (word) { return addPair(Pair.Pair(41,1)); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/047rgb.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/047rgb.solc new file mode 100644 index 00000000..a6a0c317 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/047rgb.solc @@ -0,0 +1,6 @@ +contract RGB { + enum Color { R, G, B } + function main() public returns (word) { + match (Color.B) { case Color.R { return 4; } case Color.G { return 2; } case Color.B { return 42; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/048rgb2.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/048rgb2.solc new file mode 100644 index 00000000..f7f09f48 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/048rgb2.solc @@ -0,0 +1,9 @@ +contract RGB { + enum Color { R, G, B } + + function fromEnum(c: Color) public returns (word) { + match (c) { case Color.R { return 4; } case Color.G { return 2; } case Color.B { return 42; } } + } + + function main() public returns (word) { return fromEnum(Color.B); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/049rgb3.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/049rgb3.solc new file mode 100644 index 00000000..36b0e197 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/049rgb3.solc @@ -0,0 +1,13 @@ +enum RGB { Red(word), Green(word), Blue(word) } + +contract RGB3 { + + function choose(c: RGB) public returns (word) { + let res: word; + match (c) { case RGB.Red(x) { assembly { res := add(x,1) } } case RGB.Green(x) { assembly { res := add(x,2) } } case RGB.Blue(x) { assembly { res := add(x,3) } } } + return res; + } + function main() public returns (word) { + return choose(RGB.Green(42)); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/06comp.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/06comp.solc new file mode 100644 index 00000000..d0bc6f82 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/06comp.solc @@ -0,0 +1,9 @@ +contract Compose { + function id(x: word) public returns (word) { return x; } + + function idid(x: word) public returns (word) { return id(id(x)); } + + function main() public returns (word) { + return idid(42); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/09not.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/09not.solc new file mode 100644 index 00000000..e8e3c4fe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/09not.solc @@ -0,0 +1,15 @@ +contract Not { + enum Bool { False, True } + + function main() public returns (word) { + return fromBool(bnot(Bool.False)); + } + + function fromBool(b: Bool) public returns (word) { + match (b) { case Bool.False { return 0; } case Bool.True { return 1; } } + } + + function bnot(b: Bool) public returns (Bool) { + match (b) { case Bool.False { return Bool.True; } case Bool.True { return Bool.False; } } + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/10negBool.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/10negBool.solc new file mode 100644 index 00000000..8d416b00 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/10negBool.solc @@ -0,0 +1,23 @@ + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } + + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + + +contract NegBool { + + function fromB(b: B) public returns (word) { + match (b) { case B.F { return 0; } case B.T { return 1; } } + } + + function main() public returns (word) { return fromB(Neg.neg(B.F)); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/11negPair.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/11negPair.solc new file mode 100644 index 00000000..45766493 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/11negPair.solc @@ -0,0 +1,40 @@ + +trait Neg { + function neg(x: a) returns (a) ; +} + +enum B { F, T } + +impl Neg { + function neg(x: B) returns (B) { + match (x) { case B.F { return B.T; } case B.T { return B.F; } } + } +} + +function fst(p: (a, b)) returns (a) { + match (p) { case (x, y) { return x; } } +} + +function snd(p: (a, b)) returns (b) { + match (p) { case (x, y) { return y; } } +} + + +impl Neg<(a, b)> where a: Neg, b: Neg { + function neg(p: (a, b)) returns (a, b) { + return (Neg.neg (fst(p)), Neg.neg(snd (p))); + } +} + +contract NegPair { + + function bnot(x: B) public returns (B) { + match (x) { case B.T { return B.F; } case B.F { return B.T; } } +} + + function fromB(b: B) public returns (word) { + match (b) { case B.F { return 0; } case B.T { return 1; } } +} + + function main() public returns (word) { return fromB(fst(Neg.neg((B.F,B.T)))); } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/120basicCounter.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/120basicCounter.solc new file mode 100644 index 00000000..046e2732 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/120basicCounter.solc @@ -0,0 +1,9 @@ +import std; +contract Counter { + counter : word; + + function main() public returns (word) { + counter = Num.add(counter, 42); + return counter; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/121counter.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/121counter.solc new file mode 100644 index 00000000..f88e18d6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/121counter.solc @@ -0,0 +1,14 @@ +// test single contract field +import * as std from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract Counter { + counter : word; + + function main() public returns (word) { + counter = std.addWord(counter, 1); + return counter; + } +} diff --git a/crates/parser/tests/fixtures/ok/spec/122counters.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/122counters.solc similarity index 84% rename from crates/parser/tests/fixtures/ok/spec/122counters.solc rename to crates/parser/tests/fixtures/corpus/ok/test/examples/spec/122counters.solc index ea866737..96cf06f4 100644 --- a/crates/parser/tests/fixtures/ok/spec/122counters.solc +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/122counters.solc @@ -7,7 +7,7 @@ contract Counter { counter1 : word; counter2 : uint256; counter3 : word; - function main() -> word { + function main() public returns (word) { counter1 += 1; counter3 += 2; return counter1 + counter3; diff --git a/crates/parser/tests/fixtures/ok/spec/123stackAndStorage.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/123stackAndStorage.solc similarity index 85% rename from crates/parser/tests/fixtures/ok/spec/123stackAndStorage.solc rename to crates/parser/tests/fixtures/corpus/ok/test/examples/spec/123stackAndStorage.solc index bbc776d0..6c367aa0 100644 --- a/crates/parser/tests/fixtures/ok/spec/123stackAndStorage.solc +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/123stackAndStorage.solc @@ -6,7 +6,7 @@ contract Counter { counter2 : uint256; counter3 : word; - function main() -> word { + function main() public returns (word) { let x: word; x = counter1 + 1; counter1 = x; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/126nanoerc20.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/126nanoerc20.solc new file mode 100644 index 00000000..df5b1ad2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/126nanoerc20.solc @@ -0,0 +1,80 @@ +import std; +import {address, uint256, mapping, Num, Add, Sub, Bounded, Eq, Ord, Typedef, ge, not} from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function caller() returns (address) { + let res: word; + assembly { + res := caller() + } + return address.address(res); +} + +function myrevert(msg: (word, word)) returns () { + match (msg) { case (str, len) { let str1 = str; let len1 = len; + assembly { mstore(0, str1) revert(0, len1) } } } +} + +function myrequire(cond: bool, msg: (word, word)) returns () { + if( not(cond) ) { myrevert(msg); } +} + +function require1(cond: bool) returns () { + myrequire (cond, (0x72657175697265313a204641494c, 14) /* "require1: FAIL" */ ); +} + + +function nop() returns () { return ();} + +contract Uint { + reserved : word; + msg_sender : address; // mock msg.sender + owner : address; + decimals : uint256; + totalSupply : uint256; + balances : mapping(address => uint256); + + function mint(amount: uint256) public returns () { + balances[owner] = Num.add(balances[owner], amount); + totalSupply = Num.add(totalSupply, amount); + } + + // function transferFrom(address src, address dst, uint256 amt) public returns (bool) + function transferFrom(src: address, dst: address, amt: uint256) public returns (bool) { + require1(ge(balances[src], amt)); + + /* + balances[src] = Num.sub(balances[src], amt); + balances[dst] = Num.add(balances[dst], amt):uint256; + */ + withdraw(src, amt); //workaround typechecker quirk + deposit(dst, amt); + return true; + } + + + function withdraw(src: address, amt: uint256) public returns () { + balances[src] = Num.sub(balances[src], amt); + } + + function deposit(dst: address, amt: uint256) public returns () { + balances[dst] = Num.add(balances[dst], amt); + } + + function init() public returns () { + owner = address.address(0x123456789abcdef); + msg_sender = caller(); + decimals = uint256.uint256(18); + } + + function main() public returns (uint256) { + init(); + mint(uint256.uint256(1000)); + let src: address = owner; + transferFrom(owner, msg_sender, uint256.uint256(42)); + + return balances[msg_sender]; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/127microerc20.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/127microerc20.solc new file mode 100644 index 00000000..6b6f8fb9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/127microerc20.solc @@ -0,0 +1,107 @@ +import std; +import {address, uint256, mapping, Num, Add, Sub, Bounded, Eq, Ord, Typedef, ge, ne, not} from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function caller() returns (address) { + let res: word; + assembly { + res := caller() + } + return address.address(res); +} + +function require1fail() returns () { + let res: word; + assembly { + mstore(0x0, 0x72657175697265313a204641494c) // "require1: FAIL" + revert(0,32) + } + return (); // for the typechecker +} + +function require1(cond: bool) returns () { + match (cond) { case false { return require1fail(); } case true { return (); } } +} + +function nop() returns () { return ();} + +contract Mini { + reserved : word; + msg_sender : address; // mock msg.sender + owner : address; + decimals : uint256; + totalSupply : uint256; + balances : mapping(address => uint256); + allowance : mapping(address => mapping(address => uint256)); + + function mint(amount: uint256) public returns () { + balances[owner] = Num.add(balances[owner], amount); + totalSupply = Num.add(totalSupply, amount); + } + +/* // original: + function transferFrom(address src, address dst, uint256 amt) public returns (bool) { + require(balanceOf[src] >= amt, "token/insufficient-balance"); + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= amt, "token/insufficient-allowance"); + allowance[src][msg.sender] -= amt; + } + + balanceOf[src] -= amt; + balanceOf[dst] += amt; + emit Transfer(src, dst, amt); + return true; + } +*/ + +// function transferFrom(src:address, dst:address, amt:uint256) -> bool { + function transferFrom(src: address, dst: address, amt: uint256) public returns (bool) { + require1(ge(balances[src], amt)); + let maxAllowance: uint256 = Num.maxVal(); + + match (Eq.eq(src, msg_sender)) { + case true { + match (ne(allowance[src][msg_sender], maxAllowance)) { + case true { require1(false); } + case false { (); } + } + } + case false { (); } + } + +/* + if ((src != msg_sender) && (allowance [src][msg_sender] != (Num.maxVal():uint256)) ) { + require1(allowance[src][msg.sender] >= amt); + } +*/ + balances[src] = Num.sub(balances[src], amt); + balances[dst] = Num.add(balances[dst], amt); + return true; + } + +/* + function approve(address usr, uint256 amt) public returns (bool) { + allowance[msg.sender][usr] = amt; + emit Approval(msg.sender, usr, amt); + return true; + } +*/ + + + function init() public returns () { + owner = address.address(0x123456789abcdef); + msg_sender = caller(); + decimals = uint256.uint256(18); + } + + function main() public returns (uint256) { + init(); + mint(uint256.uint256(1000)); + allowance[owner][msg_sender] = uint256.uint256(10000); + transferFrom(owner, msg_sender, uint256.uint256(42)); + + return balances[msg_sender]; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/128minierc20.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/128minierc20.solc new file mode 100644 index 00000000..00bcbf28 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/128minierc20.solc @@ -0,0 +1,99 @@ +import std; +import {address, uint256, mapping, Num, Add, Sub, Bounded, Eq, Ord, Typedef, ge, ne, not} from std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +function caller() returns (address) { + let res: word; + assembly { + res := caller() + } + return address.address(res); +} + +function myrevert(msg: word) returns () { + assembly { mstore(0, msg) revert(0, 32) } +} + +function myrequire(cond: bool, msg: word) returns () { + if( !cond ) { myrevert(msg); } +} + +contract MiniERC20 { + reserved : word; // forge idiosyncrasies + owner : address; + decimals : uint256; + totalSupply : uint256; + balances : mapping(address => uint256); + allowance : mapping(address => mapping(address => uint256)); + + function mint(amount: uint256) public returns () { + balances[owner] = Num.add(balances[owner], amount); + totalSupply = Num.add(totalSupply, amount); + } + +/* // original: + function transferFrom(address src, address dst, uint256 amt) public returns (bool) { + myrequire(balanceOf[src] >= amt, "token/insufficient-balance"); + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + myrequire(allowance[src][msg.sender] >= amt, "token/insufficient-allowance"); + allowance[src][msg.sender] -= amt; + } + + balanceOf[src] -= amt; + balanceOf[dst] += amt; + emit Transfer(src, dst, amt); + return true; + } +*/ + + function transferFrom(src: address, dst: address, amt: uint256) public returns (bool) { + let msg_sender = caller(); + let maxAllowance: uint256 = Num.maxVal(); + myrequire( balances[src] >= amt /* "token/insufficient-balance" */ + , 0x746f6b656e2f696e73756666696369656e742d62616c616e6365 + ); + + if (src != msg_sender && allowance[src][msg_sender] != maxAllowance) { + myrequire( allowance[src][msg_sender] >= amt /* "token/insufficient-allowance" */ + , 0x746f6b656e2f696e73756666696369656e742d616c6c6f77616e6365 + ); + allowance[src][msg_sender] -= amt; + } + balances[src] = balances[src] - amt; + balances[dst] = balances[dst] + amt; + return true; + } + +/* + function approve(address usr, uint256 amt) public returns (bool) { + allowance[msg.sender][usr] = amt; + emit Approval(msg.sender, usr, amt); + return true; + } +*/ + + function approve(usr: address, amt: uint256) public returns (bool) { + let msg_sender = caller(); + allowance[msg_sender][usr] = amt; + // emit Approval(msg.sender, usr, amt); + return true; + + } + + function init() public returns () { + owner = address.address(0x123456789abcdef); + decimals = uint256.uint256(18); // Num.fromWord(18) fails, which may be a problem + } + + function main() public returns (uint256) { + let msg_sender = caller(); + init(); + mint(uint256.uint256(1000)); + allowance[owner][msg_sender] = uint256.uint256(1000); + transferFrom(owner, msg_sender, uint256.uint256(42)); + + return allowance[owner][msg_sender]; + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/903badassign.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/903badassign.solc new file mode 100644 index 00000000..1c0bc506 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/903badassign.solc @@ -0,0 +1,19 @@ +contract Option { + enum Option { None, Some(a) } + + function just(x: word) public returns (Option) { return Option.Some(x); } + + function maybe(n: word, o: Option) public returns (word) { + match (o) { case Option.None { return n; } case Option.Some(x) { return x; } } + } + + function join(mmx: Option>) public returns (Option) { + let result = Option.None; + match (mmx) { case Option.Some(Option.Some(x)) { result = Option.Some(x); } case Option.None { result = Option.None; } case Option.Some(Option.None) { result = Option.None; } default { result = Option.None; } } + return result; + } + + function main() public returns (word) { + return maybe(0, join(Option.Some(Option.Some(42)))); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/939badfood.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/939badfood.solc new file mode 100644 index 00000000..85872693 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/939badfood.solc @@ -0,0 +1,17 @@ +trait Enum { + function fromEnum(x: a) returns (word) ; +} + +enum Food { Curry, Beans, Other } + +impl Enum { + function fromEnum(x: Food) returns (word) { + match (x) { case Food.Curry { return 1; } case Food.Beans { return 2; } case Food.Other { return 3; } } + } +} + +contract FoodContract { + function main() public returns (word) { + return Enum.fromEnum(Food.Beans); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/SimpleField.solc b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/SimpleField.solc new file mode 100644 index 00000000..69e20b63 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/examples/spec/SimpleField.solc @@ -0,0 +1,16 @@ +import std; +pragma solcore noPattersonCondition; +pragma solcore noCoverageCondition; +pragma solcore noBoundVariableCondition; + +contract Simple { + myval : word; + + function getVal() public returns (word) { + return myval ; + } + + function main() public returns (word) { + return getVal(); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_dup.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_dup.solc new file mode 100644 index 00000000..7eadac1d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_dup.solc @@ -0,0 +1,6 @@ +import * as M from ambA; +import * as M from ambB; + +function main(x: word) returns (word) { + return M.pick(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_hides_original_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_hides_original_fail.solc new file mode 100644 index 00000000..5defac0d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_hides_original_fail.solc @@ -0,0 +1,5 @@ +import * as FB from foo.bar; + +function main() returns (word) { + return foo.bar.value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_constr_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_constr_fail.solc new file mode 100644 index 00000000..ace9467c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_constr_fail.solc @@ -0,0 +1,7 @@ +import * as B from booldef; + +function mkTrue() returns (B.Bool) { + return True; +} + +// migrate-syntax: keep-unqualified-constructor diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_fun_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_fun_fail.solc new file mode 100644 index 00000000..41b8d3ba --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_fun_fail.solc @@ -0,0 +1,5 @@ +import * as F from foo; + +function main() returns (word) { + return base(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_type_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_type_fail.solc new file mode 100644 index 00000000..04f7a96d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/alias_unqualified_type_fail.solc @@ -0,0 +1,5 @@ +import * as B from booldef; + +function idBool(b: Bool) returns (Bool) { + return b; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/ambA.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/ambA.solc new file mode 100644 index 00000000..f3adceb8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/ambA.solc @@ -0,0 +1,5 @@ +export { pick }; + +function pick(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/ambB.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/ambB.solc new file mode 100644 index 00000000..f3adceb8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/ambB.solc @@ -0,0 +1,5 @@ +export { pick }; + +function pick(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_main.solc new file mode 100644 index 00000000..c0e3d961 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_main.solc @@ -0,0 +1,6 @@ +import {pick} from ambA; +import {pick} from ambB; + +function main(x: word) returns (word) { + return pick(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_ok.solc new file mode 100644 index 00000000..f4c115b9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/amb_ok.solc @@ -0,0 +1,6 @@ +import * as ambA from ambA; +import * as ambB from ambB; + +function main(x: word) returns (word) { + return ambA.pick(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias.solc new file mode 100644 index 00000000..03a32680 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias.solc @@ -0,0 +1,5 @@ +import * as B from booldef; + +function fromAlias(b: B.Bool) returns (B.Bool) { + return B.not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias_open_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias_open_fail.solc new file mode 100644 index 00000000..69d5ce7a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolalias_open_fail.solc @@ -0,0 +1,5 @@ +import * as B from booldef; + +function bad(b: Bool) returns (Bool) { + return not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolaliastype.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolaliastype.solc new file mode 100644 index 00000000..778718cf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolaliastype.solc @@ -0,0 +1,5 @@ +import * as B from booldef; + +function fromAliasType(b: B.Bool) returns (B.Bool) { + return B.not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_fail.solc new file mode 100644 index 00000000..0f090aa9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_fail.solc @@ -0,0 +1,7 @@ +import {Bool} from booldef; + +function mkTrue() returns (Bool) { + return True; +} + +// migrate-syntax: keep-unqualified-constructor diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_ok.solc new file mode 100644 index 00000000..2fbfea96 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolconselect_ok.solc @@ -0,0 +1,5 @@ +import {Bool} from booldef; + +function mkTrue() returns (Bool) { + return Bool.True; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/booldef.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/booldef.solc new file mode 100644 index 00000000..73293fc6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/booldef.solc @@ -0,0 +1,19 @@ +export { Bool(*), not, C, D, id }; + +enum Bool { True, False } + +function not(b: Bool) returns (Bool) { + match (b) { case Bool.True { return Bool.False; } case Bool.False { return Bool.True; } } +} + +trait C { + function c(x: a, y: a) returns (word) ; +} + +trait D { + function d() returns (a) ; +} + +function id(x: a) returns (word) where a: C, a: D { + return C.c(x, D.d()); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolmain.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolmain.solc new file mode 100644 index 00000000..db7581b9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolmain.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function and(b1: booldef.Bool, b2: booldef.Bool) returns (booldef.Bool) { + return b1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualified.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualified.solc new file mode 100644 index 00000000..32c328df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualified.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function fromQualified(b: booldef.Bool) returns (booldef.Bool) { + return booldef.not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualifiedtype.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualifiedtype.solc new file mode 100644 index 00000000..f15c86d3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolqualifiedtype.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function fromQualifiedType(b: booldef.Bool) returns (booldef.Bool) { + return booldef.not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/boolselect.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolselect.solc new file mode 100644 index 00000000..bc70b013 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/boolselect.solc @@ -0,0 +1,5 @@ +import {Bool, not} from booldef; + +function fromSelect(b: Bool) returns (Bool) { + return not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleA.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleA.solc new file mode 100644 index 00000000..28908d98 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleA.solc @@ -0,0 +1,7 @@ +import * as cycleB from cycleB; +export { fromCycleA }; +export cycleB.{fromCycleB}; + +function fromCycleA() returns (word) { + return cycleB.fromCycleB(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleB.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleB.solc new file mode 100644 index 00000000..e76676fd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycleB.solc @@ -0,0 +1,7 @@ +import * as cycleA from cycleA; +export { fromCycleB }; +export cycleA.{fromCycleA}; + +function fromCycleB() returns (word) { + return 2; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/cycle_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycle_main.solc new file mode 100644 index 00000000..72f52713 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/cycle_main.solc @@ -0,0 +1,5 @@ +import * as cycleA from cycleA; + +function main() returns (word) { + return cycleA.fromCycleB(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_context_expr.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_context_expr.solc new file mode 100644 index 00000000..539da224 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_context_expr.solc @@ -0,0 +1,11 @@ +import * as dot_left from dot_left; +import * as dot_right from dot_right; + +function mkLeft() returns (dot_left.LeftOpt) { + let x: dot_left.LeftOpt = dot_left.LeftOpt.Some(1); + return x; +} + +function main() returns (word) { + match (mkLeft()) { case dot_left.LeftOpt.Some(v) { return v; } case dot_left.LeftOpt.None { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_left.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_left.solc new file mode 100644 index 00000000..30203ed9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_left.solc @@ -0,0 +1,3 @@ +export { LeftOpt(*) }; + +enum LeftOpt { None, Some(word) } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_right.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_right.solc new file mode 100644 index 00000000..8cc9becd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dot_right.solc @@ -0,0 +1,3 @@ +export { RightOpt(*) }; + +enum RightOpt { None, Some(word) } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_a.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_a.solc new file mode 100644 index 00000000..61eae222 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_a.solc @@ -0,0 +1,5 @@ +export { foo }; + +function foo(x: word) returns (word) { + return 1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_b.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_b.solc new file mode 100644 index 00000000..87b4d50d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_b.solc @@ -0,0 +1,5 @@ +export { foo }; + +function foo(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_main.solc new file mode 100644 index 00000000..e957ae83 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_main.solc @@ -0,0 +1,7 @@ +import * as m1 from dupqual_a; +import * as m2 from dupqual_b; + +function main(x: word) returns (word) { + let y = m1.foo(x); + return m2.foo(y); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_module_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_module_main.solc new file mode 100644 index 00000000..13f8a60d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/dupqual_module_main.solc @@ -0,0 +1,7 @@ +import * as dupqual_a from dupqual_a; +import * as dupqual_b from dupqual_b; + +function main(x: word) returns (word) { + let y = dupqual_a.foo(x); + return dupqual_b.foo(y); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/export_item_dup_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/export_item_dup_fail.solc new file mode 100644 index 00000000..d6d7abb8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/export_item_dup_fail.solc @@ -0,0 +1,6 @@ +export ambA.{pick}; +export ambB.{pick}; + +function main(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/export_module_dup_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/export_module_dup_fail.solc new file mode 100644 index 00000000..95cbdc5a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/export_module_dup_fail.solc @@ -0,0 +1,6 @@ +export foo as M; +export booldef as M; + +function main() returns (word) { + return 0; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_alias_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_alias_main.solc new file mode 100644 index 00000000..7bc7882a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_alias_main.solc @@ -0,0 +1,5 @@ +import * as MathApi from @extlib.math.api; + +function main() returns (word) { + return MathApi.sum(39); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_main.solc new file mode 100644 index 00000000..819cf9b0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_main.solc @@ -0,0 +1,9 @@ +import * as api from @extlib.math.api; + +contract External { + constructor() {} + + function main() public returns (word) { + return api.sum(39); + } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_missing_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_missing_fail.solc new file mode 100644 index 00000000..f2248f40 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/external_lib_missing_fail.solc @@ -0,0 +1,5 @@ +import * as api from @missing.math.api; + +contract Missing { + constructor() {} +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/api.solc new file mode 100644 index 00000000..4782774d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/api.solc @@ -0,0 +1,8 @@ +import * as add from internals.add; +import * as util from lib.util; + +export {sum}; + +function sum(x: word) returns (word) { + return add.inc(x) + util.offset(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/internals/add.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/internals/add.solc new file mode 100644 index 00000000..dd5b16fd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/math/internals/add.solc @@ -0,0 +1,7 @@ +import {Add} from std; + +export {inc}; + +function inc(x: word) returns (word) { + return x + 1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/util.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/util.solc new file mode 100644 index 00000000..de93f722 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/extlib/util.solc @@ -0,0 +1,5 @@ +export {offset}; + +function offset() returns (word) { + return 2; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/foo.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo.solc new file mode 100644 index 00000000..ebef1581 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo.solc @@ -0,0 +1,5 @@ +export { base }; + +function base() returns (word) { + return 3; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar.solc new file mode 100644 index 00000000..b0daa7d9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar.solc @@ -0,0 +1,5 @@ +export { value }; + +function value() returns (word) { + return 7; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar/baz.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar/baz.solc new file mode 100644 index 00000000..aa7fac4b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/foo/bar/baz.solc @@ -0,0 +1,5 @@ +export { deep }; + +function deep() returns (word) { + return 9; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_a.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_a.solc new file mode 100644 index 00000000..6970ebc0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_a.solc @@ -0,0 +1,5 @@ +export {*}; + +function shared(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_b.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_b.solc new file mode 100644 index 00000000..6970ebc0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_b.solc @@ -0,0 +1,5 @@ +export {*}; + +function shared(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_main_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_main_fail.solc new file mode 100644 index 00000000..bc0e912d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_amb_main_fail.solc @@ -0,0 +1,6 @@ +import glob_amb_a; +import glob_amb_b; + +function main(x: word) returns (word) { + return shared(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_export_mixed.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_export_mixed.solc new file mode 100644 index 00000000..28313d8b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_export_mixed.solc @@ -0,0 +1,5 @@ +export {*, main}; + +function main(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_hiding_amb_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_hiding_amb_ok.solc new file mode 100644 index 00000000..7a633d56 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_hiding_amb_ok.solc @@ -0,0 +1,5 @@ +import {shared} from glob_amb_b; + +function main(x: word) returns (word) { + return shared(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_dup.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_dup.solc new file mode 100644 index 00000000..bf66acc0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_dup.solc @@ -0,0 +1,5 @@ +import globlib; + +function main(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding.solc new file mode 100644 index 00000000..f7fa2860 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding.solc @@ -0,0 +1,6 @@ +import {T, mkT} from globlib; + +function main(x: word) returns (word) { + let y: T = mkT(x); + match (y) { case T.T(v) { return v; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding_unknown_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding_unknown_fail.solc new file mode 100644 index 00000000..2004eabf --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_hiding_unknown_fail.solc @@ -0,0 +1,5 @@ +import {missing} from globlib; + +function main(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_mixed.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_mixed.solc new file mode 100644 index 00000000..631d30f3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_mixed.solc @@ -0,0 +1,5 @@ +import globlib; + +function main(x: word) returns (word) { + return idWord(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_ok.solc new file mode 100644 index 00000000..45711bf9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/glob_import_ok.solc @@ -0,0 +1,6 @@ +import globlib; + +function main(x: word) returns (word) { + let y: T = mkT(x); + match (y) { case T.T(v) { return idWord(v); } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/globlib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/globlib.solc new file mode 100644 index 00000000..431b3dd2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/globlib.solc @@ -0,0 +1,11 @@ +export {*, T(*)}; + +enum T { T(word) } + +function idWord(x: word) returns (word) { + return x; +} + +function mkT(x: word) returns (T) { + return T.T(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_dot_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_dot_fail.solc new file mode 100644 index 00000000..56bdb5dd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_dot_fail.solc @@ -0,0 +1,5 @@ +import {Token} from hidden_ctor_lib; + +function main() returns (Token) { + return Token.Err(1); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_expr_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_expr_fail.solc new file mode 100644 index 00000000..c2df2197 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_expr_fail.solc @@ -0,0 +1,5 @@ +import {Token} from hidden_ctor_lib; + +function main() returns (Token) { + return Token.Err(0); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_lib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_lib.solc new file mode 100644 index 00000000..038b849d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_lib.solc @@ -0,0 +1,11 @@ +export {Token(Ok), mkOk, mkErr}; + +enum Token { Ok(word), Err(word) } + +function mkOk(x: word) returns (Token) { + return Token.Ok(x); +} + +function mkErr(x: word) returns (Token) { + return Token.Err(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_nonexhaustive_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_nonexhaustive_fail.solc new file mode 100644 index 00000000..61c5cecb --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_nonexhaustive_fail.solc @@ -0,0 +1,5 @@ +import {Token, mkOk} from hidden_ctor_lib; + +function main() returns (word) { + match (mkOk(1)) { case Token.Ok(v) { return v; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_pattern_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_pattern_fail.solc new file mode 100644 index 00000000..0c143b56 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_pattern_fail.solc @@ -0,0 +1,5 @@ +import {Token, mkErr} from hidden_ctor_lib; + +function main() returns (word) { + match (mkErr(1)) { case Token.Err(v) { return v; } default { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_wildcard_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_wildcard_ok.solc new file mode 100644 index 00000000..7fdfaefe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/hidden_ctor_wildcard_ok.solc @@ -0,0 +1,5 @@ +import {Token, mkErr} from hidden_ctor_lib; + +function main() returns (word) { + match (mkErr(1)) { case Token.Ok(v) { return v; } default { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/import_std_minimal.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/import_std_minimal.solc new file mode 100644 index 00000000..5646ef3e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/import_std_minimal.solc @@ -0,0 +1,3 @@ +import * as std from std; + +function main() returns () {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_a.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_a.solc new file mode 100644 index 00000000..982d6b27 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_a.solc @@ -0,0 +1,5 @@ +export { fromA }; + +function fromA() returns (word) { + return 1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_b.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_b.solc new file mode 100644 index 00000000..3b997491 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_b.solc @@ -0,0 +1,5 @@ +export { fromB }; + +function fromB() returns (word) { + return fromA(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_main.solc new file mode 100644 index 00000000..54b7b821 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/leak_main.solc @@ -0,0 +1,6 @@ +import * as leak_a from leak_a; +import * as leak_b from leak_b; + +function main() returns (word) { + return fromB(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/api.solc new file mode 100644 index 00000000..732437a2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/api.solc @@ -0,0 +1,3 @@ +import * as helper from helper; + +export helper.{T}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/helper.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/helper.solc new file mode 100644 index 00000000..f03838e7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/mirror/helper.solc @@ -0,0 +1,3 @@ +export {T}; + +enum T { T } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_name_shadow.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_name_shadow.solc new file mode 100644 index 00000000..303ec7db --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_name_shadow.solc @@ -0,0 +1,9 @@ +import * as keep from foo; + +function keep() returns (word) { + return 1; +} + +function main() returns (word) { + return keep(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor.solc new file mode 100644 index 00000000..0ba9e13f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function mk() returns (booldef.Bool) { + return booldef.Bool.True; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_alias.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_alias.solc new file mode 100644 index 00000000..83b06e50 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_alias.solc @@ -0,0 +1,5 @@ +import * as b from booldef; + +function mk() returns (b.Bool) { + return b.Bool.True; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_pattern.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_pattern.solc new file mode 100644 index 00000000..df1df599 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_qualified_constructor_pattern.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function main(x: booldef.Bool) returns (word) { + match (x) { case booldef.Bool.True { return 1; } default { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_constr_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_constr_fail.solc new file mode 100644 index 00000000..1874744f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_constr_fail.solc @@ -0,0 +1,7 @@ +import * as booldef from booldef; + +function mkTrue() returns (booldef.Bool) { + return True; +} + +// migrate-syntax: keep-unqualified-constructor diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_fun_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_fun_fail.solc new file mode 100644 index 00000000..d0c62fa1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_fun_fail.solc @@ -0,0 +1,5 @@ +import * as foo from foo; + +function main() returns (word) { + return base(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_type_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_type_fail.solc new file mode 100644 index 00000000..03853f47 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/module_unqualified_type_fail.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function idBool(b: Bool) returns (Bool) { + return b; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_alias.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_alias.solc new file mode 100644 index 00000000..1e207504 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_alias.solc @@ -0,0 +1,5 @@ +import * as FB from foo.bar; + +function main() returns (word) { + return FB.value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_deep_qualifier.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_deep_qualifier.solc new file mode 100644 index 00000000..789b2327 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_deep_qualifier.solc @@ -0,0 +1,5 @@ +import * as baz from foo.bar.baz; + +function main() returns (word) { + return baz.deep(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_direct_qualifier.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_direct_qualifier.solc new file mode 100644 index 00000000..7c383fc1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_direct_qualifier.solc @@ -0,0 +1,5 @@ +import * as bar from foo.bar; + +function main() returns (word) { + return bar.value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_foo_and_bar.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_foo_and_bar.solc new file mode 100644 index 00000000..da39cc83 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_foo_and_bar.solc @@ -0,0 +1,8 @@ +import * as foo from foo; +import * as Bar from foo.bar; + +function main() returns (word) { + let x: word = foo.base(); + let y: word = Bar.value(); + return y; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_select.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_select.solc new file mode 100644 index 00000000..b90ecff1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/nested_select.solc @@ -0,0 +1,5 @@ +import {value} from foo.bar; + +function main() returns (word) { + return value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_constr_dup.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_constr_dup.solc new file mode 100644 index 00000000..6099075c --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_constr_dup.solc @@ -0,0 +1,6 @@ +enum Foo { Same } +enum Bar { Same } + +function main() returns (word) { + return 0; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_cross_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_cross_ok.solc new file mode 100644 index 00000000..ca4d7e96 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/ns_cross_ok.solc @@ -0,0 +1,5 @@ +enum Foo { Foo } + +function main() returns (Foo) { + return Foo.Foo; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_leak_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_leak_fail.solc new file mode 100644 index 00000000..2db0ff03 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_leak_fail.solc @@ -0,0 +1,5 @@ +import * as M from opaque_alias_mid; + +function bad(x: word) returns (T) { + return M.make(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_main.solc new file mode 100644 index 00000000..d0c8f9f2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_main.solc @@ -0,0 +1,6 @@ +import * as M from opaque_alias_mid; + +function main(x: word) returns (word) { + let t = M.make(x); + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_mid.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_mid.solc new file mode 100644 index 00000000..2c071630 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_mid.solc @@ -0,0 +1,7 @@ +import * as Base from opaque_dep_base; + +export { make }; + +function make(x: word) returns (Base.T) { + return Base.mkT(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_qualifier_leak_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_qualifier_leak_fail.solc new file mode 100644 index 00000000..2646b7b2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_alias_qualifier_leak_fail.solc @@ -0,0 +1,5 @@ +import * as M from opaque_alias_mid; + +function bad(x: word) returns (Base.T) { + return M.make(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_dep_base.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_dep_base.solc new file mode 100644 index 00000000..390f62d1 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_dep_base.solc @@ -0,0 +1,7 @@ +export { T(*), mkT }; + +enum T { T(word) } + +function mkT(x: word) returns (T) { + return T.T(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_main.solc new file mode 100644 index 00000000..51e23026 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_main.solc @@ -0,0 +1,6 @@ +import * as M from opaque_select_alias_mid; + +function main(x: word) returns (word) { + let t = M.make(x); + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_mid.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_mid.solc new file mode 100644 index 00000000..9ca5df84 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_alias_mid.solc @@ -0,0 +1,7 @@ +import {T as U, mkT} from opaque_dep_base; + +export { make }; + +function make(x: word) returns (U) { + return mkT(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_leak_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_leak_fail.solc new file mode 100644 index 00000000..af1c1343 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_leak_fail.solc @@ -0,0 +1,5 @@ +import * as M from opaque_select_direct_mid; + +function bad(x: word) returns (T) { + return M.make(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_mid.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_mid.solc new file mode 100644 index 00000000..dce75db0 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/opaque_select_direct_mid.solc @@ -0,0 +1,7 @@ +import {T, mkT} from opaque_dep_base; + +export { make }; + +function make(x: word) returns (T) { + return mkT(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_lib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_lib.solc new file mode 100644 index 00000000..70140bbe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_lib.solc @@ -0,0 +1,7 @@ +export { helper }; + +pragma solcore noPattersonCondition C; + +function helper() returns (word) { + return 1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_main.solc new file mode 100644 index 00000000..00e2318e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/pragma_scope_main.solc @@ -0,0 +1,7 @@ +import * as pragma_scope_lib from pragma_scope_lib; + +enum List { Nil, Cons(a, List) } + +trait C {} + +impl C, a, List> {} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_lib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_lib.solc new file mode 100644 index 00000000..fd65036a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_lib.solc @@ -0,0 +1,9 @@ +export {ok}; + +function ok() returns (word) { + return 1; +} + +function broken() returns (word) { + return true; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_main.solc new file mode 100644 index 00000000..342b6756 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_bad_main.solc @@ -0,0 +1,5 @@ +import * as private_bad_lib from private_bad_lib; + +function main() returns (word) { + return private_bad_lib.ok(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_a.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_a.solc new file mode 100644 index 00000000..e002f7f4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_a.solc @@ -0,0 +1,9 @@ +export { foo }; + +function helper(x: word) returns (word) { + return x; +} + +function foo(x: word) returns (word) { + return helper(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_main.solc new file mode 100644 index 00000000..2e3b6d34 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/private_helper_main.solc @@ -0,0 +1,5 @@ +import * as private_helper_a from private_helper_a; + +function main(x: word) returns (word) { + return private_helper_a.foo(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_hidden_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_hidden_fail.solc new file mode 100644 index 00000000..087327f9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_hidden_fail.solc @@ -0,0 +1,5 @@ +import * as reexport_ctor_mid from reexport_ctor_mid; + +function main() returns (reexport_ctor_mid.Token) { + return reexport_ctor_mid.Token.Err(1); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_ok.solc new file mode 100644 index 00000000..8fcfde40 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_expr_ok.solc @@ -0,0 +1,5 @@ +import * as reexport_ctor_mid from reexport_ctor_mid; + +function main() returns (reexport_ctor_mid.Token) { + return reexport_ctor_mid.Token.Ok(1); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_hidden_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_hidden_fail.solc new file mode 100644 index 00000000..0e5a965b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_hidden_fail.solc @@ -0,0 +1,3 @@ +import * as hidden_ctor_lib from hidden_ctor_lib; + +export hidden_ctor_lib.{Token(Err)}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_mid.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_mid.solc new file mode 100644 index 00000000..a78fdb29 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_mid.solc @@ -0,0 +1,4 @@ +import * as hidden_ctor_lib from hidden_ctor_lib; + +export hidden_ctor_lib.{Token(Ok)}; +export hidden_ctor_lib.{mkErr}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_pattern.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_pattern.solc new file mode 100644 index 00000000..1b903ac4 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_ctor_pattern.solc @@ -0,0 +1,5 @@ +import * as reexport_ctor_mid from reexport_ctor_mid; + +function main() returns (word) { + match (reexport_ctor_mid.mkErr(1)) { case reexport_ctor_mid.Token.Ok(v) { return v; } default { return 0; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/api.solc new file mode 100644 index 00000000..b4bbcb39 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/api.solc @@ -0,0 +1 @@ +export lib.reexport_items.pkg.util.{unwrap, Wrap(*), Unbox}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/util.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/util.solc new file mode 100644 index 00000000..06f84f34 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items/pkg/util.solc @@ -0,0 +1,17 @@ +export {Wrap(*), unwrap, Unbox}; + +enum Wrap { Mk(word) } + +trait Unbox { + function unbox(x: self) returns (word) ; +} + +impl Unbox { + function unbox(x: Wrap) returns (word) { + match (x) { case Wrap.Mk(w) { return w; } } + } +} + +function unwrap(x: Wrap) returns (word) { + return Unbox.unbox(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items_main.solc new file mode 100644 index 00000000..c8bc5b42 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_items_main.solc @@ -0,0 +1,5 @@ +import {unwrap, Wrap} from reexport_items.pkg.api; + +function main() returns (word) { + return unwrap(Wrap.Mk(1)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api.solc new file mode 100644 index 00000000..46908ce9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api.solc @@ -0,0 +1 @@ +export lib.reexport_module.pkg.util; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api_alias.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api_alias.solc new file mode 100644 index 00000000..7297ad47 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/api_alias.solc @@ -0,0 +1 @@ +export lib.reexport_module.pkg.util as Utils; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/util.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/util.solc new file mode 100644 index 00000000..06f84f34 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module/pkg/util.solc @@ -0,0 +1,17 @@ +export {Wrap(*), unwrap, Unbox}; + +enum Wrap { Mk(word) } + +trait Unbox { + function unbox(x: self) returns (word) ; +} + +impl Unbox { + function unbox(x: Wrap) returns (word) { + match (x) { case Wrap.Mk(w) { return w; } } + } +} + +function unwrap(x: Wrap) returns (word) { + return Unbox.unbox(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_alias_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_alias_main.solc new file mode 100644 index 00000000..3c13c305 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_alias_main.solc @@ -0,0 +1,5 @@ +import * as api_alias from reexport_module.pkg.api_alias; + +function main() returns (word) { + return api_alias.Utils.unwrap(api_alias.Utils.Wrap.Mk(1)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_main.solc new file mode 100644 index 00000000..1dc1d7fe --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_module_main.solc @@ -0,0 +1,5 @@ +import * as api from reexport_module.pkg.api; + +function main() returns (word) { + return api.util.unwrap(api.util.Wrap.Mk(1)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_main.solc new file mode 100644 index 00000000..970c881d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_main.solc @@ -0,0 +1,5 @@ +import {keep_} from reexport_select_alias_wrapper; + +function main(x: word) returns (word) { + return keep_(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_wrapper.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_wrapper.solc new file mode 100644 index 00000000..fac4bf0f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_alias_wrapper.solc @@ -0,0 +1,3 @@ +import {keep as keep_} from selectlib; + +export { keep_ }; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_base.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_base.solc new file mode 100644 index 00000000..310722d5 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_base.solc @@ -0,0 +1,5 @@ +export { mstore }; + +function mstore(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_main.solc new file mode 100644 index 00000000..9a033021 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_main.solc @@ -0,0 +1,5 @@ +import {mstore} from reexport_select_wrapper; + +function main(x: word) returns (word) { + return mstore(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_wrapper.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_wrapper.solc new file mode 100644 index 00000000..097fb4bd --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/reexport_select_wrapper.solc @@ -0,0 +1,3 @@ +import {mstore} from reexport_select_base; + +export { mstore }; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/main.solc new file mode 100644 index 00000000..606529f6 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/main.solc @@ -0,0 +1,5 @@ +import * as provider from lib.rootcheck.provider; + +function main() returns (word) { + return provider.value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/provider.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/provider.solc new file mode 100644 index 00000000..c49577b8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/provider.solc @@ -0,0 +1,5 @@ +export {value}; + +function value() returns (word) { + return 11; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/relative_and_lib_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/relative_and_lib_main.solc new file mode 100644 index 00000000..738e6eb9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/nested/relative_and_lib_main.solc @@ -0,0 +1,7 @@ +import * as provider from provider; +import * as RootProvider from lib.rootcheck.provider; + +function main() returns (word) { + let rootValue: word = RootProvider.value(); + return provider.value(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/provider.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/provider.solc new file mode 100644 index 00000000..aaa99865 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/rootcheck/provider.solc @@ -0,0 +1,5 @@ +export {value}; + +function value() returns (word) { + return 7; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_item_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_item_ok.solc new file mode 100644 index 00000000..38ab250e --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_item_ok.solc @@ -0,0 +1,5 @@ +import {keep as keep_} from selectlib; + +function main(x: word) returns (word) { + return keep_(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_multi_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_multi_ok.solc new file mode 100644 index 00000000..33de2fb7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_alias_multi_ok.solc @@ -0,0 +1,5 @@ +import {keep as keep_, drop as drop_} from selectlib; + +function main(x: word) returns (word) { + return drop_(keep_(x)); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_dup_item.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_dup_item.solc new file mode 100644 index 00000000..e87f1e4d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_dup_item.solc @@ -0,0 +1,5 @@ +import {keep, keep} from selectlib; + +function main(x: word) returns (word) { + return keep(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_fail.solc new file mode 100644 index 00000000..a718ca67 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_fail.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib; + +function main(x: word) returns (word) { + return drop(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_fail.solc new file mode 100644 index 00000000..a718ca67 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_fail.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib; + +function main(x: word) returns (word) { + return drop(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_ok.solc new file mode 100644 index 00000000..13152352 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_hiding_ok.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib; + +function main(x: word) returns (word) { + return keep(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_ok.solc new file mode 100644 index 00000000..13152352 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_ok.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib; + +function main(x: word) returns (word) { + return keep(x); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_local.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_local.solc new file mode 100644 index 00000000..276a1664 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_local.solc @@ -0,0 +1,9 @@ +import {keep} from selectlib; + +function keep() returns (word) { + return 10; +} + +function main() returns (word) { + return keep(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_param_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_param_ok.solc new file mode 100644 index 00000000..e12cbea2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_shadow_param_ok.solc @@ -0,0 +1,5 @@ +import {keep} from selectlib; + +function main(keep: word) returns (word) { + return keep; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/select_unknown.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_unknown.solc new file mode 100644 index 00000000..677074db --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/select_unknown.solc @@ -0,0 +1,5 @@ +import {missing} from selectlib; + +function main(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/selective_unqualified_fun_ok.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/selective_unqualified_fun_ok.solc new file mode 100644 index 00000000..eb490d13 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/selective_unqualified_fun_ok.solc @@ -0,0 +1,5 @@ +import {base} from foo; + +function main() returns (word) { + return base(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/selectlib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/selectlib.solc new file mode 100644 index 00000000..0ef4bed9 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/selectlib.solc @@ -0,0 +1,9 @@ +export { keep, drop }; + +function keep(x: word) returns (word) { + return x; +} + +function drop(x: word) returns (word) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/selfcycle.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/selfcycle.solc new file mode 100644 index 00000000..bd3f6487 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/selfcycle.solc @@ -0,0 +1,5 @@ +import * as selfcycle from selfcycle; + +function main() returns (word) { + return 0; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/strict_open_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/strict_open_fail.solc new file mode 100644 index 00000000..6015c140 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/strict_open_fail.solc @@ -0,0 +1,5 @@ +import * as booldef from booldef; + +function bad(b: Bool) returns (Bool) { + return not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_identity_fail.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_identity_fail.solc new file mode 100644 index 00000000..eb2f260b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_identity_fail.solc @@ -0,0 +1,6 @@ +import * as Vendor from vendor.math.api; +import * as Mirror from mirror.api; + +function bad(x: Vendor.T) returns (Mirror.T) { + return x; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_impl/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_impl/api.solc new file mode 100644 index 00000000..732437a2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/symlink_impl/api.solc @@ -0,0 +1,3 @@ +import * as helper from helper; + +export helper.{T}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_base.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_base.solc new file mode 100644 index 00000000..7f973e9a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_base.solc @@ -0,0 +1,5 @@ +export { g }; + +function g() returns (word) { + return 1; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_module.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_module.solc new file mode 100644 index 00000000..fbe4ea8b --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_module.solc @@ -0,0 +1,5 @@ +import * as M from transitive_dep_mid; + +function main() returns (word) { + return M.f(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_select.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_select.solc new file mode 100644 index 00000000..cb0010df --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_main_select.solc @@ -0,0 +1,5 @@ +import {f} from transitive_dep_mid; + +function main() returns (word) { + return f(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_mid.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_mid.solc new file mode 100644 index 00000000..8b1f863f --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/transitive_dep_mid.solc @@ -0,0 +1,7 @@ +import {g} from transitive_dep_base; + +export { f }; + +function f() returns (word) { + return g(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_a.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_a.solc new file mode 100644 index 00000000..49de8fea --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_a.solc @@ -0,0 +1,7 @@ +export { T(A), mk }; + +enum T { A } + +function mk() returns (T) { + return T.A; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_b.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_b.solc new file mode 100644 index 00000000..26cdca89 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_b.solc @@ -0,0 +1,7 @@ +export { T(B), mk }; + +enum T { B } + +function mk() returns (T) { + return T.B; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_main.solc new file mode 100644 index 00000000..ea5a130a --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/type_collision_main.solc @@ -0,0 +1,8 @@ +import * as type_collision_a from type_collision_a; +import * as type_collision_b from type_collision_b; + +function main() returns (word) { + let x = type_collision_a.mk(); + let y = type_collision_b.mk(); + return 0; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_lib.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_lib.solc new file mode 100644 index 00000000..eaafc96d --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_lib.solc @@ -0,0 +1,7 @@ +export { Bool(*), not }; + +enum Bool { True, False } + +function not(b: Bool) returns (Bool) { + match (b) { case Bool.True { return Bool.False; } case Bool.False { return Bool.True; } } +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_main.solc new file mode 100644 index 00000000..1a289537 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/unordered_imports_main.solc @@ -0,0 +1,9 @@ +export { main }; + +pragma solcore noPattersonCondition; + +function main(b: unordered_imports_lib.Bool) returns (unordered_imports_lib.Bool) { + return unordered_imports_lib.not(b); +} + +import * as unordered_imports_lib from unordered_imports_lib; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/api.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/api.solc new file mode 100644 index 00000000..732437a2 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/api.solc @@ -0,0 +1,3 @@ +import * as helper from helper; + +export helper.{T}; diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/helper.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/helper.solc new file mode 100644 index 00000000..f03838e7 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/vendor/math/helper.solc @@ -0,0 +1,3 @@ +export {T}; + +enum T { T } diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/wildA.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/wildA.solc new file mode 100644 index 00000000..524ae153 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/wildA.solc @@ -0,0 +1,6 @@ +import * as wildB from wildB; +export {wildB.*, *}; + +function fromWildA() returns (word) { + return wildB.fromWildB(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/wildB.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/wildB.solc new file mode 100644 index 00000000..f1e4aa79 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/wildB.solc @@ -0,0 +1,6 @@ +import * as wildA from wildA; +export {wildA.*, *}; + +function fromWildB() returns (word) { + return 3; +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/wild_main.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/wild_main.solc new file mode 100644 index 00000000..be854052 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/wild_main.solc @@ -0,0 +1,5 @@ +import * as wildA from wildA; + +function main() returns (word) { + return wildA.fromWildB(); +} diff --git a/crates/parser/tests/fixtures/corpus/ok/test/imports/wrapper_shadow_success.solc b/crates/parser/tests/fixtures/corpus/ok/test/imports/wrapper_shadow_success.solc new file mode 100644 index 00000000..3d676ed8 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/ok/test/imports/wrapper_shadow_success.solc @@ -0,0 +1,9 @@ +import * as booldef from booldef; + +function not(x: word) returns (word) { + return x; +} + +function main(b: booldef.Bool) returns (booldef.Bool) { + return booldef.not(b); +} diff --git a/crates/parser/tests/fixtures/corpus/reference-frontend.tsv b/crates/parser/tests/fixtures/corpus/reference-frontend.tsv new file mode 100644 index 00000000..1e3e1fe3 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/reference-frontend.tsv @@ -0,0 +1,434 @@ +path status code +Convertible.solc fail SC0001 +cases/Ackermann.solc pass +cases/Add1.solc pass +cases/BadInstance.solc fail SC0102 +cases/BoolNot.solc pass +cases/Compose.solc pass +cases/Compose3.solc pass +cases/CondExp.solc pass +cases/DupFun.solc fail SC0108 +cases/DuplicateFun.solc pass +cases/EitherModule.solc pass +cases/Enum.solc fail SC0108 +cases/Eq.solc fail SC0102 +cases/EqQual.solc pass +cases/EvenOdd.solc pass +cases/Filter.solc fail SC0102 +cases/Foo.solc pass +cases/GetSet.solc fail SC0103 +cases/GoodInstance.solc fail SC0102 +cases/Id.solc pass +cases/IncompleteInstDef.solc fail SC0299 +cases/Invokable.solc fail SC0102 +cases/KindTest.solc fail SC0103 +cases/ListModule.solc pass +cases/Logic.solc pass +cases/MatchCall.solc pass +cases/Memory1.solc pass +cases/Memory2.solc pass +cases/Mutuals.solc pass +cases/NegPair.solc pass +cases/Option.solc pass +cases/Pair.solc pass +cases/PairMatch1.solc fail SC0209 +cases/PairMatch2.solc fail SC0209 +cases/Peano.solc pass +cases/PeanoMatch.solc pass +cases/Ref.solc fail SC0102 +cases/RefDeref.solc pass +cases/SillyReturn.solc fail SC0220 +cases/SimpleInvoke.solc fail SC0102 +cases/SimpleLambda.solc pass +cases/SingleFun.solc pass +cases/StructMembers.solc fail SC0001 +cases/Uncurry.solc pass +cases/abigeneric.solc pass +cases/add-moritz.solc fail SC0102 +cases/another-subst.solc pass +cases/app.solc pass +cases/array.solc pass +cases/asm-assign-no-return.solc fail SC0220 +cases/asm-assign-non-word.solc fail SC0001 +cases/asm-let-bool-lit.solc pass +cases/asm-let-no-return.solc fail SC0220 +cases/asm-let-uninit.solc pass +cases/asm-match-tuple-read.solc pass +cases/asm-match-tuple-write-read.solc pass +cases/assembly.solc pass +cases/bal.solc pass +cases/bar.solc pass +cases/bitwise.solc pass +cases/bool-elim.solc pass +cases/bound-merge-case.solc pass +cases/bound-minimal.solc fail SC0103 +cases/bound-only-test.solc fail SC0103 +cases/bound-with-pragma.solc pass +cases/bug-import-default-inst-shadow.solc pass +cases/bug-rep-name-capture.solc pass +cases/bug-spec-generic-let.solc fail +cases/catch-all.solc pass +cases/catenable-err.solc fail SC0001 +cases/class-context.solc pass +cases/class-return-type-miss.solc fail SC0221 +cases/class-type-name-collision.solc fail SC0108 +cases/closure-capture-only.solc pass +cases/closure-free-bound-test.solc pass +cases/closure-free-var-local.solc pass +cases/closure-free-var-std.solc pass +cases/closure-free-var.solc pass +cases/closure.solc pass +cases/comp.solc fail SC0220 +cases/comparisons.solc pass +cases/complexproxy.solc fail SC0102 +cases/compose0.solc pass +cases/compose_desugared.solc fail SC0209 +cases/const-array.solc fail SC0221 +cases/const.solc pass +cases/constrained-instance-context.solc pass +cases/constrained-instance.solc pass +cases/constructor-weak-args.solc pass +cases/copytomem.solc pass +cases/cyclical-defs-inferred.solc pass +cases/cyclical-defs.solc pass +cases/default-inst.solc fail SC0102 +cases/default-instance-missing.solc fail SC0102 +cases/default-instance-weak.solc fail SC0102 +cases/derive-generic-excluded.solc pass +cases/derive-generic-sum.solc pass +cases/derive-self-return-poc.solc fail SC0001 +cases/dispatch.solc fail SC0103 +cases/dot-expression-assignment-context.solc pass +cases/dot-expression-call-arg-context.solc pass +cases/dot-expression-constructor.solc pass +cases/dot-expression-match-return.solc pass +cases/dot-expression-nested-context.solc pass +cases/dot-expression-no-context-fail.solc fail SC0224 +cases/dot-expression-unknown-fail.solc fail SC0224 +cases/dot-pattern-constructor.solc pass +cases/dot-pattern-nested-constructor.solc pass +cases/dot-primitive-constructor.solc pass +cases/duplicated-contract-name.solc fail SC0108 +cases/duplicated-type-name.solc fail SC0108 +cases/empty-asm.solc pass +cases/encoder.solc pass +cases/encoder1.solc pass +cases/fallback-with-args.solc fail SC0001 +cases/fallback-with-return.solc fail SC0001 +cases/false-redundant-warning.solc pass +cases/field-access.solc fail SC0201 +cases/field-helper-cxt-collision.solc pass +cases/field-name-error.solc pass +cases/foo-class.solc pass +cases/for-body-shadow.solc pass +cases/for-break.solc pass +cases/for-continue.solc pass +cases/for-empty-init.solc pass +cases/for-init-shadow.solc pass +cases/for-inner-block.solc pass +cases/for-let-post.solc fail SC0001 +cases/for-let.solc pass +cases/for-loop.solc pass +cases/for-multi-init.solc pass +cases/for-multi-post.solc pass +cases/fresh-pat-arg-synonym.solc pass +cases/fresh-pat-arg.solc pass +cases/fresh-variable-shadowing.solc pass +cases/generic-manual-no-pragma.solc fail +cases/generic-product-no-pragma.solc fail +cases/generic-sum-no-pragma.solc fail +cases/if-examples.solc pass +cases/import-std.solc pass +cases/inc-closure.solc pass +cases/index-example.solc fail SC0108 +cases/instance-closure-error-invalid-member.solc fail SC0201 +cases/instance-closure-error.solc pass +cases/instance-context-wrong-kind.solc fail SC0299 +cases/instance-synonym-int.solc pass +cases/instance-synonym.solc pass +cases/instance-wrong-sig.solc fail SC0299 +cases/invokable-issue.solc pass +cases/ixa.solc pass +cases/join.solc pass +cases/joinErr.solc fail SC0201 +cases/listeq.solc fail SC0220 +cases/listid.solc pass +cases/ltimp.solc pass +cases/ltproxy.solc pass +cases/mainproxy.solc fail SC0102 +cases/match-bitwise.solc pass +cases/match-compiler-undef-asm.solc fail SC0299 +cases/match-yul.solc pass +cases/memory.solc pass +cases/missing-instance.solc fail SC0223 +cases/mod-example.solc pass +cases/modifier.solc pass +cases/modulo.solc pass +cases/monomorphic-require.solc pass +cases/morefun.solc pass +cases/mptc-both-templates.solc pass +cases/mptc-chain-phantom.solc pass +cases/mptc-guard-extras-concrete.solc pass +cases/mptc-multi-instance.solc pass +cases/mptc-nop-mainty-free.solc pass +cases/mptc-partial-instance.solc pass +cases/mptc-template-a-only.solc pass +cases/mptc-template-b-only.solc pass +cases/multi-stmt-var-leaf.solc pass +cases/nano-desugared.solc fail SC0108 +cases/nid.solc pass +cases/noclosure.solc pass +cases/noconstr.solc fail SC0102 +cases/notif.solc pass +cases/option2.solc pass +cases/overlap-synonym-detected.solc fail SC0299 +cases/overlap-synonym-missed-order.solc fail SC0299 +cases/overlap-synonym-missed-two-synonyms.solc fail SC0299 +cases/overlapping-heads.solc fail SC0299 +cases/pair-bug.solc pass +cases/pars.solc pass +cases/patterson-bug.solc fail SC0108 +cases/payable-toplevel-function.solc fail SC0001 +cases/phantom-type-return-con.solc pass +cases/polymatch-error.solc pass +cases/polymorphic-require.solc pass +cases/pragma_merge_base.solc pass +cases/pragma_merge_fail_coverage.solc fail SC0299 +cases/pragma_merge_fail_patterson.solc fail SC0105 +cases/pragma_merge_import.solc fail SC0105 +cases/pragma_merge_verify.solc fail SC0105 +cases/pragma_test_patterson.solc pass +cases/proxy-desugar.solc pass +cases/proxy.solc pass +cases/proxy1.solc fail SC0223 +cases/public-constructor.solc fail SC0001 +cases/public-fallback.solc fail SC0001 +cases/public-top-level-function.solc fail SC0001 +cases/rec.solc pass +cases/redundant-match.solc pass +cases/reference-encoding-good.solc pass +cases/reference-encoding-good1.solc pass +cases/reference-encoding.solc fail SC0102 +cases/reference-test.solc fail SC0102 +cases/reference.solc fail SC0001 +cases/references-daniel.solc fail SC0102 +cases/require-annotation-contract-method.solc fail SC0220 +cases/require-annotation-missing-both.solc fail SC0220 +cases/require-annotation-missing-param.solc fail SC0220 +cases/require-annotation-missing-return.solc fail SC0220 +cases/require-annotation-mutual.solc fail SC0220 +cases/return-fun-adder.solc pass +cases/return-fun-bad-arity.solc fail SC0201 +cases/return-fun-bad-param.solc fail SC0201 +cases/return-fun-bad-return.solc fail SC0201 +cases/return-fun-bad-sig.solc fail SC0201 +cases/return-fun-const.solc pass +cases/return-fun-eq.solc pass +cases/return-fun-instance.solc pass +cases/return-fun-not-fun.solc fail SC0201 +cases/same-name-constructor-qualifier.solc pass +cases/signature.solc fail SC0001 +cases/simpleDiscount.solc pass +cases/simpleIfExpr.solc fail SC0220 +cases/simpleIfStmt.solc fail SC0220 +cases/simpleid.solc pass +cases/single-lambda.solc pass +cases/skolem-let.solc fail SC0209 +cases/snds.solc pass +cases/spec-fail-ungrounded.solc pass +cases/strange-unbound.solc pass +cases/string-const.solc fail SC0220 +cases/subject-index.solc fail SC0108 +cases/subject-reduction.solc fail SC0108 +cases/subsumption-constraint.solc fail SC0223 +cases/subsumption-test.solc fail SC0209 +cases/sum-match-default.solc pass +cases/super-class-cycle-fail.solc fail SC0223 +cases/super-class-cycle.solc pass +cases/super-class-num.solc pass +cases/super-class-recursive-arg.solc fail SC0223 +cases/super-class.solc pass +cases/synonym-arity-mismatch.solc fail SC0299 +cases/synonym-basic.solc pass +cases/synonym-in-function.solc pass +cases/synonym-long-cycle.solc fail SC0299 +cases/synonym-nested.solc pass +cases/synonym-param.solc pass +cases/synonym-recursive.solc fail SC0299 +cases/synonym-self-recursive.solc fail SC0299 +cases/tabled-answer-reuse.solc fail SC0299 +cases/tabled-cycle-fail.solc timeout +cases/tabled-default-instance.solc pass +cases/tabled-given-order.solc pass +cases/tabled-left-recursive-fail.solc timeout +cases/tabled-mutual-chain.solc fail SC0299 +cases/tabled-residual-given.solc pass +cases/td.solc pass +cases/tiamat.solc pass +cases/toplevel-constructor.solc fail SC0001 +cases/toplevel-fallback.solc fail SC0001 +cases/tuple-trick.solc pass +cases/tuva.solc pass +cases/tyexp.solc pass +cases/type-synonym-arg.solc pass +cases/typedef.solc pass +cases/uintdesugared.solc pass +cases/unbound-instance-var.solc fail SC0103 +cases/unconstrained-instance.solc fail SC0001 +cases/undefined.solc pass +cases/unit.solc pass +cases/user-op-lambda.solc fail SC0001 +cases/vartyped.solc fail SC0220 +cases/weird-error-foo.solc fail SC0220 +cases/weirdfoo.solc fail SC0001 +cases/word-match-default.solc pass +cases/word-match.solc pass +cases/xref.solc fail SC0221 +cases/yul-asm-for-body.solc pass +cases/yul-asm-switch-body.solc pass +cases/yul-deposit-example.solc pass +cases/yul-for.solc pass +cases/yul-function-typing.solc pass +cases/yul-multi-return-arity-fail.solc fail SC0299 +cases/yul-multi-return.solc pass +cases/yul-return.solc pass +comptime/CondExpr.solc pass +comptime/CondStmt.solc pass +comptime/OneOne.solc fail SC0001 +comptime/OneTwo.solc pass +comptime/Plus.solc pass +comptime/Size.solc pass +comptime/StdSize.solc pass +comptime/comptime_syntax.solc pass +comptime/counter.solc pass +comptime/ct_asm_mem.solc pass +comptime/ct_asm_ret.solc pass +comptime/ct_chain_ok.solc pass +comptime/ct_let_ok.solc pass +comptime/ct_let_runtime.solc pass +comptime/ct_overloaded_bad.solc pass +comptime/ct_overloaded_ok.solc pass +comptime/ct_param_ok.solc pass +comptime/ct_param_poly_runtime.solc fail SC0299 +comptime/ct_param_runtime.solc fail +comptime/ct_runtime_arg.solc pass +comptime/fib.solc pass +comptime/fib2.solc pass +comptime/fib3.solc pass +comptime/fromInt.solc fail SC0103 +comptime/fromInt2.solc fail SC0103 +comptime/fromInt3.solc fail SC0103 +comptime/fromLit.solc fail SC0103 +comptime/int-untyped-let.solc pass +comptime/integer-basic.solc pass +comptime/integer-fib.solc pass +comptime/integer-from-integer.solc pass +comptime/integer-lit-class.solc pass +comptime/integer-lit-cond.solc pass +comptime/integer-lit-pat.solc pass +comptime/integer-lit-poly.solc pass +comptime/integer-lit-safe.solc pass +comptime/integer-lit-word-site.solc pass +comptime/integer-lit.solc pass +comptime/match_labels.solc pass +comptime/string-lit-keccak.solc pass +comptime/string-lit-len.solc pass +comptime/string-lit-ops.solc pass +comptime/uint256-lit.solc pass +dispatch/Revert.solc pass +dispatch/assembly.solc pass +dispatch/basic.solc pass +dispatch/concat.solc pass +dispatch/counter.solc pass +dispatch/ecrecover.solc pass +dispatch/empty.solc pass +dispatch/empty_no_constructor.solc pass +dispatch/fallback.solc pass +dispatch/fib.solc fail SC0103 +dispatch/forloops.solc pass +dispatch/generic_product.solc pass +dispatch/generic_sum.solc pass +dispatch/hashes.solc pass +dispatch/memory.solc pass +dispatch/miniERC20.solc pass +dispatch/neg.solc pass +dispatch/nonpayable_ctor.solc pass +dispatch/ownable.solc pass +dispatch/payable.solc pass +dispatch/payable_ctor.solc pass +dispatch/slices.solc pass +dispatch/specialise_sum_of_product.solc pass +dispatch/storage.solc pass +dispatch/stringid.solc pass +dispatch/sum_wide_product.solc pass +dispatch/weth9.solc pass +invokable/021nid.solc fail SC0220 +invokable/022nid-invoke.solc fail SC0001 +invokable/024lamid.solc fail SC0220 +invokable/025lamid-invoke.solc fail SC0001 +invokable/026capture.solc fail SC0001 +invokable/027retfun.solc fail SC0001 +invokable/028modifier.solc fail SC0001 +invokable/031enum.solc fail SC0001 +opcodes/all-shapes.solc pass +pragmas/bound.solc fail SC0001 +pragmas/coverage.solc pass +pragmas/patterson.solc pass +spec/00answer.solc pass +spec/010answer.solc fail SC0220 +spec/011id.solc fail SC0220 +spec/012nid.solc fail SC0220 +spec/013comp.solc fail SC0220 +spec/01id.solc pass +spec/021not.solc pass +spec/022add.solc pass +spec/024arith.solc pass +spec/027sstore.solc fail SC0220 +spec/02nid.solc pass +spec/031maybe.solc pass +spec/032simplejoin.solc pass +spec/033join.solc pass +spec/034cojoin.solc pass +spec/035padding.solc pass +spec/036wildcard.solc pass +spec/037dwarves.solc pass +spec/038food0.solc pass +spec/039food.solc pass +spec/041pair.solc pass +spec/042triple.solc pass +spec/043fstsnd.solc pass +spec/047rgb.solc pass +spec/048rgb2.solc pass +spec/049rgb3.solc pass +spec/051expreturn.solc fail SC0103 +spec/051negBool.solc fail SC0102 +spec/052negPair.solc fail SC0001 +spec/052return.solc fail SC0103 +spec/053return.solc fail SC0103 +spec/06comp.solc pass +spec/09not.solc pass +spec/101struct1Field.solc fail SC0102 +spec/102uintField.solc fail SC0102 +spec/103struct3Fields.solc fail SC0102 +spec/105nestedStruct.solc fail SC0102 +spec/10negBool.solc pass +spec/111storageStruct.solc fail SC0102 +spec/112ContractStorage.solc fail SC0105 +spec/113counter.solc fail SC0105 +spec/11negPair.solc pass +spec/120basicCounter.solc pass +spec/121counter.solc pass +spec/122counters.solc pass +spec/123stackAndStorage.solc pass +spec/126nanoerc20.solc pass +spec/127microerc20.solc pass +spec/128minierc20.solc pass +spec/131constructor.solc fail SC0220 +spec/135cons3.solc fail SC0108 +spec/903badassign.solc pass +spec/939badfood.solc pass +spec/SimpleField.solc pass +spec/StorageLib.solc fail SC0220 +spec/attic/051expreturn.solc fail SC0001 +spec/attic/052return.solc fail SC0001 +spec/attic/053return.solc fail SC0001 diff --git a/crates/parser/tests/fixtures/corpus/rust-accepted-reference-failures.tsv b/crates/parser/tests/fixtures/corpus/rust-accepted-reference-failures.tsv new file mode 100644 index 00000000..c2b22811 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/rust-accepted-reference-failures.tsv @@ -0,0 +1,10 @@ +# pathreason +cases/class-return-type-miss.solc SC0221 reference treats omitted trait and impl results as incomplete; new syntax defines both omitted results as unit and Rust accepts the matching signatures +cases/Eq.solc SC0102 reference rejects the legacy constrained class form; its explicit new-syntax trait binder and completed impl signature are accepted by Rust +cases/compose_desugared.solc SC0209 reference rejects the explicitly desugared closure because its inferred invoke implementation is not polymorphic enough; Rust accepts it +cases/for-let-post.solc SC0001 reference frontend rejects this for-loop let form while the Rust grammar accepts it +cases/super-class-recursive-arg.solc SC0223 reference legacy solver rejects this recursive superclass argument, while the reference tabled mode and Rust both accept it +cases/tabled-answer-reuse.solc SC0299 reference legacy solver reports an ambiguous inferred type, while the reference tabled mode and Rust both accept it +cases/tabled-mutual-chain.solc SC0299 reference legacy solver reports an ambiguous inferred type, while the reference tabled mode and Rust both accept it +comptime/ct_param_poly_runtime.solc SC0299 reference legacy frontend reports ambiguity; both tabled Haskell and Rust reject the runtime argument during their full specialization pipelines, while this frontend-only parity gate intentionally defers that check +spec/131constructor.solc SC0220 reference treats an omitted constructor result as incomplete; new syntax defines the omitted result as unit and Rust accepts it diff --git a/crates/parser/tests/fixtures/corpus/rust-rejected-reference-passes.tsv b/crates/parser/tests/fixtures/corpus/rust-rejected-reference-passes.tsv new file mode 100644 index 00000000..6bab6e56 --- /dev/null +++ b/crates/parser/tests/fixtures/corpus/rust-rejected-reference-passes.tsv @@ -0,0 +1,68 @@ +# pathphasediagnostic-prefixreason +cases/Uncurry.solc typeck SC0206: non-callable value of type word legacy reference accepts invoking a word-typed parameter; Rust frontend deliberately requires a callable type +cases/field-helper-cxt-collision.solc typeck SC0101: undefined name: Contract reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0101: undefined name: Fallback reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0101: undefined name: Method reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0101: undefined name: RunContract reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0101: undefined name: fallback_default_implementation reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0103: undefined type constructor: NonPayable reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/field-helper-cxt-collision.solc typeck SC0105: undefined trait: SigString reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/ixa.solc typeck SC0221: invalid impl member signature for `size` reference accepts this legacy impl member with a narrowed array signature; Rust frontend enforces the declared trait signature +cases/multi-stmt-var-leaf.solc typeck SC0236: contract runtime `main` must not take parameters reference accepts the legacy parameterized contract main form; Rust frontend reserves runtime main as a zero-argument entrypoint +cases/pair-bug.solc typeck SC0101: undefined name: Contract reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/pair-bug.solc typeck SC0101: undefined name: Fallback reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/pair-bug.solc typeck SC0101: undefined name: RunContract reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/pair-bug.solc typeck SC0101: undefined name: fallback_default_implementation reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/pair-bug.solc typeck SC0103: undefined type constructor: NonPayable reference verdict used -g and skipped generated dispatch; the Rust full-frontend gate generates it, but this fixture does not import std.dispatch +cases/rec.solc typeck SC0206: non-callable value of type word legacy reference accepts invoking a word-typed parameter; Rust frontend deliberately requires a callable type +imports/alias_dup.solc frontend SC0116: duplicate import qualifier `M` intentional negative import fixture for duplicate aliases +imports/alias_hides_original_fail.solc frontend SC0101: undefined name: foo intentional negative import fixture proving an alias hides the original qualifier +imports/alias_unqualified_constr_fail.solc frontend SC0106: unqualified constructor: True intentional negative import fixture proving module imports do not open constructors +imports/alias_unqualified_fun_fail.solc frontend SC0101: undefined name: base intentional negative import fixture proving aliased module imports do not open terms +imports/alias_unqualified_type_fail.solc frontend SC0103: undefined type constructor: Bool intentional negative import fixture proving aliased module imports do not open types +imports/amb_main.solc frontend SC0120: ambiguous selected import `pick` in term namespace intentional negative import fixture for ambiguous selected imports +imports/boolalias_open_fail.solc frontend SC0101: undefined name: not intentional negative import fixture proving an aliased module import is not an open import +imports/boolalias_open_fail.solc frontend SC0103: undefined type constructor: Bool intentional negative import fixture proving an aliased module import is not an open import +imports/boolconselect_fail.solc frontend SC0106: unqualified constructor: True intentional negative import fixture proving a selected type import does not expose its constructors +imports/export_item_dup_fail.solc frontend SC0111: duplicate exported item name `pick` intentional negative import fixture for duplicate item re-exports +imports/export_module_dup_fail.solc frontend SC0112: duplicate exported module name `M` intentional negative import fixture for duplicate module re-exports +imports/external_lib_missing_fail.solc frontend SC0118: external library root is not configured: @missing intentional negative import fixture for an unconfigured external library +imports/external_lib_missing_fail.solc frontend unresolved-import: external_lib_missing_fail imports `@missing.math.api` intentional negative import fixture for an unconfigured external library +imports/external_lib_missing_fail.solc typeck SC0101: undefined name: Contract unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0101: undefined name: Fallback unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0101: undefined name: Proxy unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0101: undefined name: RunContract unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0101: undefined name: fallback_default_implementation unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0103: undefined type constructor: NonPayable unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/external_lib_missing_fail.solc typeck SC0103: undefined type constructor: Proxy unresolved external import intentionally causes generated contract-helper cascades in this negative fixture +imports/glob_amb_main_fail.solc frontend SC0120: ambiguous selected import `shared` in term namespace intentional negative import fixture for colliding wildcard imports +imports/glob_import_hiding_unknown_fail.solc frontend SC0110: unknown import item `missing` intentional negative import fixture for hiding an unknown wildcard-imported name +imports/hidden_ctor_dot_fail.solc frontend SC0101: undefined name: Err intentional negative import fixture proving hidden constructors are unavailable to dot syntax +imports/hidden_ctor_expr_fail.solc frontend SC0101: undefined name: Err intentional negative import fixture proving hidden constructors are unavailable in expressions +imports/hidden_ctor_nonexhaustive_fail.solc typeck SC0223: pattern match on type with hidden constructors requires a wildcard arm: Token intentional negative import fixture for exhaustiveness with a partially visible data type +imports/hidden_ctor_pattern_fail.solc frontend SC0101: undefined name: Token.Err intentional negative import fixture proving hidden constructors are unavailable in patterns +imports/leak_b.solc frontend SC0101: undefined name: fromA intentional negative import fixture proving private imported terms do not leak through an intermediate module +imports/leak_main.solc frontend SC0101: undefined name: fromA intentional negative import fixture proving private imported terms do not leak through an intermediate module +imports/leak_main.solc frontend SC0101: undefined name: fromB intentional negative import fixture proving private imported terms do not leak through an intermediate module +imports/module_name_shadow.solc frontend SC0121: conflicting unqualified name `keep` intentional negative import fixture for a module qualifier colliding with a selected term +imports/module_unqualified_constr_fail.solc frontend SC0106: unqualified constructor: True intentional negative import fixture proving module imports do not open constructors +imports/module_unqualified_fun_fail.solc frontend SC0101: undefined name: base intentional negative import fixture proving module imports do not open terms +imports/module_unqualified_type_fail.solc frontend SC0103: undefined type constructor: Bool intentional negative import fixture proving module imports do not open types +imports/opaque_alias_leak_fail.solc frontend SC0103: undefined type constructor: T intentional negative import fixture proving opaque type aliases do not leak through imports +imports/opaque_alias_qualifier_leak_fail.solc frontend SC0103: undefined type constructor: Base.T intentional negative import fixture proving opaque type aliases do not leak through qualifiers +imports/opaque_select_direct_leak_fail.solc frontend SC0103: undefined type constructor: T intentional negative import fixture proving opaque type aliases cannot be selected through re-exports +imports/pragma_scope_main.solc typeck SC0212: Coverage condition fails for trait: intentional negative import fixture proving a dependency pragma does not disable checks in its importer +imports/private_bad_lib.solc typeck SC0201: type mismatch: expected word, found bool intentional negative import fixture containing a type error in a private helper body +imports/private_bad_main.solc typeck SC0201: type mismatch: expected word, found bool intentional negative import fixture proving reachable private helper bodies are type-checked +imports/reexport_ctor_expr_hidden_fail.solc frontend SC0101: undefined name: Err intentional negative import fixture proving hidden constructors do not leak through re-exports +imports/reexport_ctor_hidden_fail.solc frontend SC0115: unknown re-exported constructor `Token.Err` intentional negative import fixture for explicitly re-exporting a hidden constructor +imports/select_dup_item.solc frontend SC0117: duplicate name `keep` in selective import intentional negative import fixture for duplicate names in one selective import +imports/select_fail.solc frontend SC0101: undefined name: drop intentional negative import fixture proving unselected terms remain unavailable +imports/select_hiding_fail.solc frontend SC0101: undefined name: drop intentional negative import fixture proving hidden selected terms remain unavailable +imports/select_shadow_local.solc frontend SC0108: duplicate declaration `keep` in term namespace intentional negative import fixture for a selected term colliding with a local declaration +imports/select_unknown.solc frontend SC0110: unknown import item `missing` intentional negative import fixture for an unknown selected item +imports/strict_open_fail.solc frontend SC0101: undefined name: not intentional negative import fixture proving a strict module import is not an open import +imports/strict_open_fail.solc frontend SC0103: undefined type constructor: Bool intentional negative import fixture proving a strict module import is not an open import +imports/symlink_identity_fail.solc typeck SC0201: type mismatch: expected Mirror.T, found T intentional negative import fixture proving equivalent source paths retain distinct module type identities +imports/symlink_impl/api.solc frontend SC0109: import helper: file not found auxiliary symlink fixture is materialized by the module-system test and is not independently complete in the checked-in corpus +imports/symlink_impl/api.solc frontend unresolved-import: failed to read auxiliary symlink fixture is materialized by the module-system test and is not independently complete in the checked-in corpus diff --git a/crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.snap b/crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.snap deleted file mode 100644 index d9877ca7..00000000 --- a/crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.solc ---- -error: unexpected `;`; expected end of input, or statement - --> /assembly_trailing_semicolon.solc:4:4 - | -3 | mstore(0, 0) -4 | }; - | ^ -5 | } - | diff --git a/crates/parser/tests/fixtures/fail/class_missing_body_brace.snap b/crates/parser/tests/fixtures/fail/class_missing_body_brace.snap deleted file mode 100644 index 27602b48..00000000 --- a/crates/parser/tests/fixtures/fail/class_missing_body_brace.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/class_missing_body_brace.solc ---- -error: unexpected end of input; expected `(`, or `{` while parsing predicate - --> /class_missing_body_brace.solc:1:13 - | -1 | class T: Eq - | ^ diff --git a/crates/parser/tests/fixtures/fail/class_missing_body_brace.solc b/crates/parser/tests/fixtures/fail/class_missing_body_brace.solc deleted file mode 100644 index 8e27f54d..00000000 --- a/crates/parser/tests/fixtures/fail/class_missing_body_brace.solc +++ /dev/null @@ -1 +0,0 @@ -class T: Eq diff --git a/crates/parser/tests/fixtures/fail/data_trailing_pipe.snap b/crates/parser/tests/fixtures/fail/data_trailing_pipe.snap deleted file mode 100644 index 7c7be5b1..00000000 --- a/crates/parser/tests/fixtures/fail/data_trailing_pipe.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/data_trailing_pipe.solc ---- -error: unexpected `;`; expected different token while parsing data declaration - --> /data_trailing_pipe.solc:1:28 - | -1 | data Option(T) = Some(T) | ; - | ^ diff --git a/crates/parser/tests/fixtures/fail/data_trailing_pipe.solc b/crates/parser/tests/fixtures/fail/data_trailing_pipe.solc deleted file mode 100644 index 6ff3e4c4..00000000 --- a/crates/parser/tests/fixtures/fail/data_trailing_pipe.solc +++ /dev/null @@ -1 +0,0 @@ -data Option(T) = Some(T) | ; diff --git a/crates/parser/tests/fixtures/fail/function_param_recovery.snap b/crates/parser/tests/fixtures/fail/function_param_recovery.snap deleted file mode 100644 index 27ac7f3d..00000000 --- a/crates/parser/tests/fixtures/fail/function_param_recovery.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/function_param_recovery.solc ---- -error: unexpected `,`; expected type while parsing function parameter - --> /function_param_recovery.solc:1:16 - | -1 | function bad(x:, y: U) {} - | ^ -2 | function ok() {} - | diff --git a/crates/parser/tests/fixtures/fail/function_param_recovery.solc b/crates/parser/tests/fixtures/fail/function_param_recovery.solc deleted file mode 100644 index 48b2f9fb..00000000 --- a/crates/parser/tests/fixtures/fail/function_param_recovery.solc +++ /dev/null @@ -1,2 +0,0 @@ -function bad(x:, y: U) {} -function ok() {} diff --git a/crates/parser/tests/fixtures/fail/function_signature_missing_type.snap b/crates/parser/tests/fixtures/fail/function_signature_missing_type.snap deleted file mode 100644 index c3dcc3bd..00000000 --- a/crates/parser/tests/fixtures/fail/function_signature_missing_type.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/function_signature_missing_type.solc ---- -error: unexpected `)`; expected type while parsing function parameter - --> /function_signature_missing_type.solc:1:17 - | -1 | function bad(x: ) {} - | ^ diff --git a/crates/parser/tests/fixtures/fail/if_trailing_semicolon.snap b/crates/parser/tests/fixtures/fail/if_trailing_semicolon.snap deleted file mode 100644 index bb1ca921..00000000 --- a/crates/parser/tests/fixtures/fail/if_trailing_semicolon.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/if_trailing_semicolon.solc ---- -error: unexpected `;`; expected `else`, end of input, or statement - --> /if_trailing_semicolon.solc:4:4 - | -3 | return (); -4 | }; - | ^ -5 | } - | diff --git a/crates/parser/tests/fixtures/fail/if_trailing_semicolon.solc b/crates/parser/tests/fixtures/fail/if_trailing_semicolon.solc deleted file mode 100644 index 2ab5a6fd..00000000 --- a/crates/parser/tests/fixtures/fail/if_trailing_semicolon.solc +++ /dev/null @@ -1,5 +0,0 @@ -function f() { - if true { - return (); - }; -} diff --git a/crates/parser/tests/fixtures/fail/instance_missing_head.snap b/crates/parser/tests/fixtures/fail/instance_missing_head.snap deleted file mode 100644 index 12ac75bb..00000000 --- a/crates/parser/tests/fixtures/fail/instance_missing_head.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/instance_missing_head.solc ---- -error: unexpected `{`; expected `(`, `=>`, or predicate while parsing instance declaration - --> /instance_missing_head.solc:1:10 - | -1 | instance {} - | ^ diff --git a/crates/parser/tests/fixtures/fail/instance_missing_head.solc b/crates/parser/tests/fixtures/fail/instance_missing_head.solc deleted file mode 100644 index d45578e9..00000000 --- a/crates/parser/tests/fixtures/fail/instance_missing_head.solc +++ /dev/null @@ -1 +0,0 @@ -instance {} diff --git a/crates/parser/tests/fixtures/fail/invalid_token.snap b/crates/parser/tests/fixtures/fail/invalid_token.snap deleted file mode 100644 index fc6b31dc..00000000 --- a/crates/parser/tests/fixtures/fail/invalid_token.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/invalid_token.solc ---- -error: invalid token `@` - --> /invalid_token.solc:1:1 - | -1 | @ - | ^ diff --git a/crates/parser/tests/fixtures/fail/invalid_token.solc b/crates/parser/tests/fixtures/fail/invalid_token.solc deleted file mode 100644 index 59c227c5..00000000 --- a/crates/parser/tests/fixtures/fail/invalid_token.solc +++ /dev/null @@ -1 +0,0 @@ -@ diff --git a/crates/parser/tests/fixtures/fail/match_trailing_semicolon.snap b/crates/parser/tests/fixtures/fail/match_trailing_semicolon.snap deleted file mode 100644 index a46e7c7b..00000000 --- a/crates/parser/tests/fixtures/fail/match_trailing_semicolon.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/match_trailing_semicolon.solc ---- -error: unexpected `;`; expected end of input, or statement - --> /match_trailing_semicolon.solc:4:4 - | -3 | | _ => return (); -4 | }; - | ^ -5 | } - | diff --git a/crates/parser/tests/fixtures/fail/match_trailing_semicolon.solc b/crates/parser/tests/fixtures/fail/match_trailing_semicolon.solc deleted file mode 100644 index 4cc77387..00000000 --- a/crates/parser/tests/fixtures/fail/match_trailing_semicolon.solc +++ /dev/null @@ -1,5 +0,0 @@ -function f() { - match 0 { - | _ => return (); - }; -} diff --git a/crates/parser/tests/fixtures/fail/missing_semicolon.snap b/crates/parser/tests/fixtures/fail/missing_semicolon.snap deleted file mode 100644 index b9056222..00000000 --- a/crates/parser/tests/fixtures/fail/missing_semicolon.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/missing_semicolon.solc ---- -error: unexpected end of input; expected `.`, or `;` while parsing import declaration - --> /missing_semicolon.solc:1:18 - | -1 | import core.math - | ^ diff --git a/crates/parser/tests/fixtures/fail/multiple_emitted_errors.snap b/crates/parser/tests/fixtures/fail/multiple_emitted_errors.snap deleted file mode 100644 index 4702785e..00000000 --- a/crates/parser/tests/fixtures/fail/multiple_emitted_errors.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/multiple_emitted_errors.solc ---- -error: invalid token `@` - --> /multiple_emitted_errors.solc:1:1 - | -1 | @ - | ^ -2 | # - | ---- - -error: invalid token `#` - --> /multiple_emitted_errors.solc:2:1 - | -1 | @ -2 | # - | ^ diff --git a/crates/parser/tests/fixtures/fail/multiple_emitted_errors.solc b/crates/parser/tests/fixtures/fail/multiple_emitted_errors.solc deleted file mode 100644 index a6f9b22f..00000000 --- a/crates/parser/tests/fixtures/fail/multiple_emitted_errors.solc +++ /dev/null @@ -1,2 +0,0 @@ -@ -# diff --git a/crates/parser/tests/fixtures/fail/multiple_errors_continue.snap b/crates/parser/tests/fixtures/fail/multiple_errors_continue.snap deleted file mode 100644 index 03fb672d..00000000 --- a/crates/parser/tests/fixtures/fail/multiple_errors_continue.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/multiple_errors_continue.solc ---- -error: unexpected `function`; expected `.`, or `;` while parsing import declaration - --> /multiple_errors_continue.solc:2:1 - | -1 | import core.math -2 | function bad() { - | ^^^^^^^^ -3 | let x = ; - | diff --git a/crates/parser/tests/fixtures/fail/pragma_missing_name.snap b/crates/parser/tests/fixtures/fail/pragma_missing_name.snap deleted file mode 100644 index 20d3c2b7..00000000 --- a/crates/parser/tests/fixtures/fail/pragma_missing_name.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/pragma_missing_name.solc ---- -error: unexpected `;`; expected different token while parsing pragma declaration - --> /pragma_missing_name.solc:1:8 - | -1 | pragma ; - | ^ diff --git a/crates/parser/tests/fixtures/fail/top_level_recovery.snap b/crates/parser/tests/fixtures/fail/top_level_recovery.snap deleted file mode 100644 index 833dd73b..00000000 --- a/crates/parser/tests/fixtures/fail/top_level_recovery.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/top_level_recovery.solc ---- -error: could not parse top-level item near `unknown nonsense tokens`; expected a declaration starting with `import`, `pragma`, `type`, `data`, `class`, `instance`, `contract`, or `function` - --> /top_level_recovery.solc:2:1 - | -1 | function first() {} -2 | unknown nonsense tokens - | ^^^^^^^^^^^^^^^^^^^^^^^ -3 | function second() {} - | diff --git a/crates/parser/tests/fixtures/fail/type_alias_missing_equals.snap b/crates/parser/tests/fixtures/fail/type_alias_missing_equals.snap deleted file mode 100644 index b07a39e5..00000000 --- a/crates/parser/tests/fixtures/fail/type_alias_missing_equals.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/parser/tests/diagnostics.rs -expression: value -input_file: crates/parser/tests/fixtures/fail/type_alias_missing_equals.solc ---- -error: unexpected identifier `U`; expected `(`, or `=` while parsing type alias declaration - --> /type_alias_missing_equals.solc:1:13 - | -1 | type Amount U; - | ^ diff --git a/crates/parser/tests/fixtures/fail/type_alias_missing_equals.solc b/crates/parser/tests/fixtures/fail/type_alias_missing_equals.solc deleted file mode 100644 index c97a30f5..00000000 --- a/crates/parser/tests/fixtures/fail/type_alias_missing_equals.solc +++ /dev/null @@ -1 +0,0 @@ -type Amount U; diff --git a/crates/parser/tests/fixtures/ok/comptime_match_label.solc b/crates/parser/tests/fixtures/ok/comptime_match_label.solc new file mode 100644 index 00000000..90b14b6f --- /dev/null +++ b/crates/parser/tests/fixtures/ok/comptime_match_label.solc @@ -0,0 +1,10 @@ +function classify(x: word) returns (word) { + match (x) { + case comptime 1 { + return 1; + } + default { + return 0; + } + } +} diff --git a/crates/parser/tests/fixtures/ok/comptime_modifier.solc b/crates/parser/tests/fixtures/ok/comptime_modifier.solc new file mode 100644 index 00000000..29355b53 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/comptime_modifier.solc @@ -0,0 +1,13 @@ +alias CompileTime = word; + +contract ComptimeModifier { + function f(comptime x: word) returns (comptime word) { + return x; + } + + function identifier(x: CompileTime) returns (CompileTime) { + let compileTimeValue: word = 1; + let comptime y: word = f(compileTimeValue); + return y; + } +} diff --git a/crates/parser/tests/fixtures/ok/contract_modifiers_constructor_fallback.solc b/crates/parser/tests/fixtures/ok/contract_modifiers_constructor_fallback.solc new file mode 100644 index 00000000..4c317846 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/contract_modifiers_constructor_fallback.solc @@ -0,0 +1,11 @@ +contract Modifiers { + constructor() {} + + function ping() public {} + + function deposit() public payable returns (uint256) { + return 0; + } + + fallback() external payable {} +} diff --git a/crates/parser/tests/fixtures/ok/dot_ctor_expr_pattern.solc b/crates/parser/tests/fixtures/ok/dot_ctor_expr_pattern.solc new file mode 100644 index 00000000..c16d4d2c --- /dev/null +++ b/crates/parser/tests/fixtures/ok/dot_ctor_expr_pattern.solc @@ -0,0 +1,19 @@ +enum Option { + None, + Some(word), +} + +function mkSome(x: word) returns (Option) { + return Option.Some(x); +} + +function fromOption(x: Option) returns (word) { + match (x) { + case Option.Some(v) { + return v; + } + case Option.None { + return 0; + } + } +} diff --git a/crates/parser/tests/fixtures/ok/export_operator_list.solc b/crates/parser/tests/fixtures/ok/export_operator_list.solc new file mode 100644 index 00000000..41e89801 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/export_operator_list.solc @@ -0,0 +1 @@ +export { f }; diff --git a/crates/parser/tests/fixtures/ok/expression_bodied.solc b/crates/parser/tests/fixtures/ok/expression_bodied.solc new file mode 100644 index 00000000..7622457a --- /dev/null +++ b/crates/parser/tests/fixtures/ok/expression_bodied.solc @@ -0,0 +1,15 @@ +function zero() returns (word) { + return 0; +} + +function apply(f: function(word) returns (word), x: word) returns (word) { + return f(x); +} + +function choose(c: bool, a: word, b: word) returns (word) { + return c ? a : b; +} + +function keepThen(then: word) returns (word) { + return then; +} diff --git a/crates/parser/tests/fixtures/ok/for_loop.solc b/crates/parser/tests/fixtures/ok/for_loop.solc new file mode 100644 index 00000000..50671a22 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/for_loop.solc @@ -0,0 +1,7 @@ +function sum10() returns (word) { + let s: word = 0; + for (let i = 1; i <= 10; i = i + 1) { + s = s + i; + } + return s; +} diff --git a/crates/parser/tests/fixtures/ok/import_alias_operator_hiding.solc b/crates/parser/tests/fixtures/ok/import_alias_operator_hiding.solc new file mode 100644 index 00000000..daf581ac --- /dev/null +++ b/crates/parser/tests/fixtures/ok/import_alias_operator_hiding.solc @@ -0,0 +1 @@ +import {A as B, power} from mod; diff --git a/crates/parser/tests/fixtures/ok/import_external_alias.solc b/crates/parser/tests/fixtures/ok/import_external_alias.solc new file mode 100644 index 00000000..11484c62 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/import_external_alias.solc @@ -0,0 +1 @@ +import * as X from @lib.a.b; diff --git a/crates/parser/tests/fixtures/ok/import_mixed_wildcard.solc b/crates/parser/tests/fixtures/ok/import_mixed_wildcard.solc new file mode 100644 index 00000000..e00a9d70 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/import_mixed_wildcard.solc @@ -0,0 +1,3 @@ +import * as Glob from glob; +import * as Glob2 from glob2; +import * as Glob3 from glob3; diff --git a/crates/parser/tests/fixtures/ok/import_wildcard_selector.solc b/crates/parser/tests/fixtures/ok/import_wildcard_selector.solc new file mode 100644 index 00000000..0b53482a --- /dev/null +++ b/crates/parser/tests/fixtures/ok/import_wildcard_selector.solc @@ -0,0 +1 @@ +import * as Mod from mod; diff --git a/crates/parser/tests/fixtures/ok/match_arm_block.solc b/crates/parser/tests/fixtures/ok/match_arm_block.solc new file mode 100644 index 00000000..e8aeda84 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/match_arm_block.solc @@ -0,0 +1,10 @@ +function main(foo: (word, word)) returns (word) { + let res: word; + match (foo) { + case (v0, v1) { + let x: word = v1; + res = x; + } + } + return res; +} diff --git a/crates/parser/tests/fixtures/ok/match_trailing_semicolon.solc b/crates/parser/tests/fixtures/ok/match_trailing_semicolon.solc new file mode 100644 index 00000000..fbcf296f --- /dev/null +++ b/crates/parser/tests/fixtures/ok/match_trailing_semicolon.solc @@ -0,0 +1,7 @@ +function f() { + match (0) { + default { + return (); + } + } +} diff --git a/crates/parser/tests/fixtures/ok/new_syntax_expr_stmt.solc b/crates/parser/tests/fixtures/ok/new_syntax_expr_stmt.solc new file mode 100644 index 00000000..2ae754d6 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/new_syntax_expr_stmt.solc @@ -0,0 +1,30 @@ +function coreSurface(x: word, y: word) returns (word) { + let comptime limit: word = 10; + let n: word = x as word as word; + let selected: word = n < y ? n : y; + let shifted = (x << 1) >> 1; + let powered = x ** 2; + + if (n < limit) { + n = n + 1; + } else { + n = y; + } + + while (n < limit) { + n += 1; + } + + unchecked { + n = n + 1; + } + + match (n, selected) { + case (0, _) { + return selected; + } + default { + revert; + } + } +} diff --git a/crates/parser/tests/fixtures/ok/new_syntax_imports_pragmas.solc b/crates/parser/tests/fixtures/ok/new_syntax_imports_pragmas.solc new file mode 100644 index 00000000..8ccc0d2c --- /dev/null +++ b/crates/parser/tests/fixtures/ok/new_syntax_imports_pragmas.solc @@ -0,0 +1,14 @@ +import std; +import std.dispatch; +import @ext.plain; +import * as dispatch from std.dispatch; +import * as externalDispatch from @ext.foo.dispatch; +import {address, uint256 as U256} from std; +import {foo, bar as baz,} from @ext.foo.bar; + +pragma solidity ^0.8.23; +pragma abicoder v2; +pragma solcore noCoverageCondition; +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; +pragma solcore noGenericInstanceFor MyType, OtherType; diff --git a/crates/parser/tests/fixtures/ok/new_syntax_items.solc b/crates/parser/tests/fixtures/ok/new_syntax_items.solc new file mode 100644 index 00000000..f543af75 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/new_syntax_items.solc @@ -0,0 +1,79 @@ +pragma solidity ^0.8.23; +pragma solcore noCoverageCondition Eq; + +alias Word = uint256; +alias PairOf = (T, T); +type Wad is uint256; + +struct Pair { + left: T; + right: T; +} + +enum Option { + None, + Some(T), +} + +enum Result { + Ok(T), + Err(E), +} + +trait Eq { + function eq(x: T, y: T) returns (bool); +} + +impl Eq { + function eq(x: uint256, y: uint256) returns (bool) { + return x == y; + } +} + +impl Eq> where T: Eq { + function eq(x: Option, y: Option) returns (bool) { + return true; + } +} + +function id(x: T) returns (T) { + return x; +} + +function namedResult() returns (result: uint256) { + return 0; +} + +function destructure(x: uint256, y: uint256) returns (uint256) { + let (left, right): (uint256, uint256) = (x, y); + return left + right; +} + +contract Token { + balances: mapping(address => uint256); + values: uint256[]; + fixedValues: uint256[4]; + callback: function(uint256) internal returns (bool); + + constructor(initialSupply: uint256) payable { + balances[msg.sender] = initialSupply; + } + + function balanceOf(account: address) public view returns (uint256) { + return balances[account]; + } + + fallback() external payable { + revert; + } +} + +interface Reader { + function read(key: uint256) external view returns (uint256); +} + +library Math { + function add(x: uint256, y: uint256) internal pure returns (uint256) { + return x + y; + } +} diff --git a/crates/parser/tests/fixtures/ok/operators_compound_assign.solc b/crates/parser/tests/fixtures/ok/operators_compound_assign.solc new file mode 100644 index 00000000..5ceda12a --- /dev/null +++ b/crates/parser/tests/fixtures/ok/operators_compound_assign.solc @@ -0,0 +1,11 @@ +function operators(x, y, z) { + let acc = x % y; + acc = (acc & y) | (x ^ z); + acc += x; + acc -= y; + acc ^= z; + acc &= x; + acc |= y; + acc %= z; + return acc; +} diff --git a/crates/parser/tests/fixtures/ok/parser_catchup_h.solc b/crates/parser/tests/fixtures/ok/parser_catchup_h.solc new file mode 100644 index 00000000..3df04698 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/parser_catchup_h.solc @@ -0,0 +1,14 @@ +enum First { + First(word), +} + +enum Second { + Second, +} + +export mod; +export mod as M; +export mod.{a}; +export { T(*) }; + +import {T} from m; diff --git a/crates/parser/tests/fixtures/ok/proxy_expression.solc b/crates/parser/tests/fixtures/ok/proxy_expression.solc new file mode 100644 index 00000000..9e41b66b --- /dev/null +++ b/crates/parser/tests/fixtures/ok/proxy_expression.solc @@ -0,0 +1,6 @@ +function main(x: word) returns (word) { + let p = @word; + let pairProxy = @(word, word); + let converted: @word = p; + return x; +} diff --git a/crates/parser/tests/fixtures/ok/proxy_type_sugar.solc b/crates/parser/tests/fixtures/ok/proxy_type_sugar.solc new file mode 100644 index 00000000..2bf9677e --- /dev/null +++ b/crates/parser/tests/fixtures/ok/proxy_type_sugar.solc @@ -0,0 +1 @@ +function proxy_sig(x: @word) returns (@word) {} diff --git a/crates/parser/tests/fixtures/ok/qualified_constructor_pattern_3_segment.solc b/crates/parser/tests/fixtures/ok/qualified_constructor_pattern_3_segment.solc new file mode 100644 index 00000000..570d01e3 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/qualified_constructor_pattern_3_segment.solc @@ -0,0 +1,10 @@ +function main(x: mod.Type.Bool) returns (word) { + match (x) { + case mod.Type.True { + return 1; + } + default { + return 0; + } + } +} diff --git a/crates/parser/tests/fixtures/ok/qualified_constructor_patterns.solc b/crates/parser/tests/fixtures/ok/qualified_constructor_patterns.solc new file mode 100644 index 00000000..f96392c3 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/qualified_constructor_patterns.solc @@ -0,0 +1,20 @@ +contract QualifiedConstructorPatterns { + enum Option { + None, + Some(T), + } + + function join(mmx: Option>) returns (Option) { + match (mmx) { + case Option.None { + return Option.None; + } + case Option.Some(Option.Some(x)) { + return Option.Some(x); + } + case Option.Some(Option.None) { + return Option.None; + } + } + } +} diff --git a/crates/parser/tests/fixtures/ok/qualified_type_return.solc b/crates/parser/tests/fixtures/ok/qualified_type_return.solc new file mode 100644 index 00000000..3ec301e7 --- /dev/null +++ b/crates/parser/tests/fixtures/ok/qualified_type_return.solc @@ -0,0 +1 @@ +function qualified_ret() returns (mod.Type) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/00answer.solc b/crates/parser/tests/fixtures/ok/solcore_examples/00answer.solc deleted file mode 100644 index f7112655..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/00answer.solc +++ /dev/null @@ -1,5 +0,0 @@ -contract Answer { - function main() { - return 42; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/Convertible.solc b/crates/parser/tests/fixtures/ok/solcore_examples/Convertible.solc deleted file mode 100644 index 9ec80dd4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/Convertible.solc +++ /dev/null @@ -1,118 +0,0 @@ -data Pair(a,b) = Pair(a,b); -data Proxy(a) = Proxy; -data Unit = Unit; - -class a:Typedef(r) { - function abs(x:r) -> a; - function rep(x:a) -> r; -} - - -data uint16 = uint16(word); - -instance uint16:Typedef(word) { - function abs(r:word) { return uint16(r);} - function rep(x: uint16) -> word { - match x { - | uint16(val) => return val; - } - } -} - -data uint8 = uint8(word); - -instance uint8:Typedef(word) { - function abs(r:word) { return uint8(r);} - function rep(x: uint8) -> word { - match x { - | uint8(val) => return val; - } - } -} - -data uint256 = uint256(word); - -instance uint256:Typedef(word) { - function abs(r:word) { return uint256(r);} - function rep(x: uint256) -> word { - match x { - | uint256(val) => return val; - } - } -} - - -function foo(x:word) -> uint16 { - let result : uint16 = Typedef.abs(x); - return result; -} - - -class self:Convertible(r) -{ - function convert(x:self) -> r; -} - -instance Pair(uint8,Proxy(uint16)):Convertible(uint16) { - function convert(p:Pair(uint8,Proxy(uint16))) -> uint16 { - match p { - | Pair(x, _) => return Typedef.abs(Typedef.rep(x)); - } - } -} - - - -function uint8to16(x : uint8) -> uint16 { - let proxy : Proxy(uint16) = Proxy; - let result : uint16 = Convertible.convert(Pair(x,proxy)); - return result; -} - -/* -forall Pair(a,Proxy(b)):Convertible(b). function convert(x:a) -> b { - let proxy : Proxy(b) = Proxy; - let result : b = Convertible.convert(Pair(x,proxy)); - return result; -} -*/ - -forall a b. function convert(x:a) -> b { - let proxy : Proxy(b) = Proxy; - let result : b = Convertible.convert(Pair(x,proxy)); - return result; -} - -function bar(x:Unit) -> word { - let result: word = convert(x); - return result; -} - - -instance Pair(uint8,Proxy(uint256)):Convertible(uint256) { - function convert(p:Pair(uint8,Proxy(uint256))) -> uint256 { - match p { - | Pair(x, _) => return Typedef.abs(Typedef.rep(x)); - } - } -} - -instance Pair(uint16,Proxy(uint256)):Convertible(uint256) { - function convert(p:Pair(uint16,Proxy(uint256))) -> uint256 { - match p { - | Pair(x, _) => return Typedef.abs(Typedef.rep(x)); - } - } -} - - - -contract Bar { - -function main() -> word { - let x = Unit; - let y : word = convert(x); - return y; -} - -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ackermann.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ackermann.solc deleted file mode 100644 index 5c413dc5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ackermann.solc +++ /dev/null @@ -1,10 +0,0 @@ -data Nat = Zero | Succ(Nat) ; - -function foo (x, y) { - match y, x { - | y1, Zero => return 1 ; - | Zero, Succ(x2) => return 2; - | Succ(y3), Succ(x3) => return 3; - } -} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Add1.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Add1.solc deleted file mode 100644 index fe34bd94..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Add1.solc +++ /dev/null @@ -1,9 +0,0 @@ -contract Add1 { - function main() { - let res: word; - assembly { - res := add(40, 2) - } - return res; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/BadInstance.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/BadInstance.solc deleted file mode 100644 index f52425eb..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/BadInstance.solc +++ /dev/null @@ -1,18 +0,0 @@ -class a:Enum { - function fromEnum(x:a) -> word; - } - -data Color = R | G | B; - -data Bool = False | True; - -instance Bool : Enum { - function fromEnum(b) { - match b { - | R => return 0; - | G => return 1; - } - } -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/BoolNot.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/BoolNot.solc deleted file mode 100644 index 421ceb58..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/BoolNot.solc +++ /dev/null @@ -1,8 +0,0 @@ -data Bool = False | True; - -function not (b) { - match b { - | False => return True ; - | True => return False ; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose.solc deleted file mode 100644 index 2985f709..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose.solc +++ /dev/null @@ -1,38 +0,0 @@ -forall a b c . function compose1(f : (b) -> c, g : (a) -> b) -> ((a) -> c) { - return lam (x) { - return f(g(x)); - } ; -} - - -forall a b c d e . d : invokable(b,c), e : invokable(a,b) => - function compose2 (f : d, g : e) -> ((a) -> c) { - return lam (x) { - return invokable.invoke(f, invokable.invoke(g,x)); - }; - } - -function compose0(f,g) { - return lam(x) { - return invokable.invoke(f, invokable.invoke(g,x)); - }; -} - -function compose3(f,g) { - return lam(x){return f(g(x));}; -} - -forall a b c . c : invokable(a,b) => function apply (f : c, x : a) -> b { - return invokable.invoke(f,x); -} - -function id(x) { - return x; -} - -contract Foo { - function main () -> word { - let f = compose3(id,id); - return f(0); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose2.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose2.solc deleted file mode 100644 index ac36564e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose2.solc +++ /dev/null @@ -1,18 +0,0 @@ -contract Compose { - function compose(f,g) { - return lam (x) { - return f(g(x)); - } ; - } - - function id(x) { return x; } - - function idid() { return compose(id,id); } - - // function main() { return idid(42); } - - function main() { - let f = compose(id,id); - return f(42); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose3.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose3.solc deleted file mode 100644 index 9ae5ac17..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Compose3.solc +++ /dev/null @@ -1,17 +0,0 @@ -contract Compose { - function compose(f,g) { - return lam (x) { - return f(g(x)); - } ; - } - - function id(x) { return x; } - - function idid() { return compose(id,id); } - - function apply1(f, a) { return f(a); } - - function main() { - return apply1(compose(id, id), 42); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/CondExp.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/CondExp.solc deleted file mode 100644 index c13afac6..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/CondExp.solc +++ /dev/null @@ -1,8 +0,0 @@ -contract CondExp { - function main() { - return - if if true then false else true - then if false then 1 else 2 - else if true then 42 else 56; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/DupFun.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/DupFun.solc deleted file mode 100644 index fbfc9dce..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/DupFun.solc +++ /dev/null @@ -1,11 +0,0 @@ -function f(x : word) -> word { - return x; -} - -function f(x : word) -> word { - return 10; -} - -function g(x : word) -> word { - return f(x); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/DuplicateFun.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/DuplicateFun.solc deleted file mode 100644 index 5ef3bb2c..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/DuplicateFun.solc +++ /dev/null @@ -1,21 +0,0 @@ - -forall self . class self:A { - function foo(p : self) -> word; -} - -forall self . class self:B { - function foo(p : self) -> word; -} - -instance word:B { - function foo(x : word) -> word { - return x; - } -} - -// error: Constraint for A not found in type of foo -instance word:A { - function foo(x : word) -> word { - return x; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EitherModule.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/EitherModule.solc deleted file mode 100644 index 2b0845d8..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EitherModule.solc +++ /dev/null @@ -1,16 +0,0 @@ -contract EitherModule { - data Either(a,b) = Left(a) | Right(b); - data List(a) = Nil | Cons(a,List(a)); - - function lefts(xs) { - match xs { - | Nil => return Nil ; - | Cons(y,ys) => - match y { - | Left(z) => return Cons(z,lefts(ys)) ; - | Right(z) => return lefts(ys) ; - } - } - } - -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Enum.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Enum.solc deleted file mode 100644 index 05bdab92..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Enum.solc +++ /dev/null @@ -1,21 +0,0 @@ -class a: Enum { - function fromEnum(a) -> word; -} - -data Food = Curry | Beans | Other; - -instance Food : Enum { - function fromEnum(x : Food) { - match x { - | Curry => return 1; - | Beans => return 2; - | Other => return 3; - } - } -} - -contract Food { - function main() { - return Enum.fromEnum(Beans); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Eq.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Eq.solc deleted file mode 100644 index 5c44d7fd..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Eq.solc +++ /dev/null @@ -1,20 +0,0 @@ -data Bool = True | False; - -class a : Eq { - function eq (x : a, y : a) -> Bool; -} - -forall a . a : Eq => class a : Ord { - function lt (x : a, y : a) -> Bool ; -} - -instance word : Eq { - function eq (x,y) { - match primEqWord(x,y) { - | 0 => - return False; - | _ => - return True ; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EqQual.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/EqQual.solc deleted file mode 100644 index 0840179f..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EqQual.solc +++ /dev/null @@ -1,24 +0,0 @@ -data Bool = True | False; - -forall a . class a : Eq { - function eq (x : a, y : a) -> Bool; -} - -forall a . a : Eq => class a : Ord { - function lt (x : a, y : a) -> Bool ; -} - -instance word : Eq { - function eq (x : word, y : word) -> Bool { - match primEqWord(x,y) { - | 0 => - return False; - | _ => - return True ; - } - } -} - -function foo (x) { - return Eq.eq (x, 0); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EvenOdd.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/EvenOdd.solc deleted file mode 100644 index 28b20820..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/EvenOdd.solc +++ /dev/null @@ -1,18 +0,0 @@ -contract EvenOdd { - data Nat = Zero | Succ(Nat); - data Bool = False | True; - - function even (n) { - match n { - | Zero => return True; - | Succ(m) => return odd(m); - } - } - - function odd(n) { - match n { - | Zero => return False; - | Succ(m) => return even(m); - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Filter.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Filter.solc deleted file mode 100644 index 796183c5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Filter.solc +++ /dev/null @@ -1,52 +0,0 @@ -data List(a) = Nil | Cons(a,List(a)); -data Bool = False | True; - -function and(x,y) { - match x, y { - | False, _ => return False; - | True, z => return z; - } -} - -class a : Eq { - function eq (x : a, y : a) -> Bool ; -} - -instance Word : Eq { - function eq (x, y) { - match primEqWord(x,y) { - | 0 => return False ; - | _ => return True ; - } - } -} - - -function filter (f, xs) { - match xs { - | Nil => return Nil ; - | Cons(y,ys) => - match f(y) { - | False => return filter(f,ys); - | True => return Cons(y,filter(f,ys)); - } - } -} - -function list1 () { - return Cons(1, Cons(2, Cons(3, Nil))); -} - -function foo0(y) { - return filter((lam (x){ return eq(x,y); }), list1()); -} - -function foo1() { - return filter((lam (x){ return eq(x,1); }), list1()); -} - -function foo2(p,q) { - return filter(lam (x) { return and(p(x), q(x)) ; } - , list1()); -} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Foo.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Foo.solc deleted file mode 100644 index 4620b038..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Foo.solc +++ /dev/null @@ -1,9 +0,0 @@ - function one() { - return primAddWord(1, zero()) ; - } - - function zero () { - return 0; - } - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/GetSet.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/GetSet.solc deleted file mode 100644 index 67bf3ad5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/GetSet.solc +++ /dev/null @@ -1,11 +0,0 @@ -contract GetSet { - value : Word ; - - function setValue (x) { - value = x ; - } - - function getValue () { - return value ; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/GoodInstance.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/GoodInstance.solc deleted file mode 100644 index 80ed5c0b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/GoodInstance.solc +++ /dev/null @@ -1,31 +0,0 @@ -class a:Enum { - function fromEnum(x:a) -> Word; -} - - data Color = R | G | B; - -instance Color : Enum { - function fromEnum(c) { - match c { - | R => return 1; - | G => return 2; - | B => return 3; - } - } -} - - -data Bool = False | True; - -instance Bool : Enum { - function fromEnum(b) { - match b { - | False => return 0; - | True => return 1; - } - } -} - -contract GoodInstance { - function main() { return fromEnum(True);} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Id.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Id.solc deleted file mode 100644 index 918cd856..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Id.solc +++ /dev/null @@ -1,12 +0,0 @@ -function id() { - return lam (x) { return x; } ; -} - -contract Id { - function main () { - let f = id(); - return f(0); - } -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/IncompleteInstDef.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/IncompleteInstDef.solc deleted file mode 100644 index 4d86d53e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/IncompleteInstDef.solc +++ /dev/null @@ -1,14 +0,0 @@ -forall a b . class a : Foo(b) { - function foo (x : a, y : b) -> b ; - function faa (y : a) -> a ; -} - -data Bool = False | True; - -data Maybe(a) = Nothing | Just(a); -// missing the definition of Foo.foo -instance Bool : Foo(Bool) { - function faa(y : Bool) -> Bool { - return y ; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/IndexLib.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/IndexLib.solc deleted file mode 100644 index acb357b7..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/IndexLib.solc +++ /dev/null @@ -1,370 +0,0 @@ -import NumLib; - -/////// Construction -forall abs rep. -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x; } -} - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data address = address(word); - -instance address:Typedef(word) { - function rep(x:address) -> word { - match x { - | address(y) => return y; - } - } - function abs(x:word) -> address { - return address(x); - } -} - -instance address:Eq { - function eq(x : address , y : address) -> Bool { - return Eq.eq(Typedef.rep(x), Typedef.rep(y)); - } -} - -data storage(a) = storage(word); -data ContractStorage(cxt) = ContractStorage(cxt); - -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); // storage by default -data mapRef(a) = mapRef(word); //ref to a map elem - -// data memoryRef(a) = memoryRef(word); - -forall a. -instance storage(a):Typedef(word) { - function rep(x:storage(a)) -> word { - match x { - | storage(y) => return y; - } - } - function abs(x:word) -> storage(a) { - return storage(x); - } -} - -forall a. -instance storageRef(a):Typedef(word) { - function rep(x:storageRef(a)) -> word { - match x { - | storageRef(y) => return y; - } - } - function abs(x:word) -> storageRef(a) { - return storageRef(x); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a. -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self. -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self. -class self:StorageSize { - function size(x:Proxy(self)) -> word; -} - - -function sload_(x:word) -> word { - let res: word; - assembly { - res := sload(x) - } - return res; - } - -function sstore_(a:word, v:word) { - assembly { sstore(a,v) } -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -instance uint:StorageType { - function sload(ptr:word) -> uint { - return Typedef.abs(sload_(ptr)); - } - function store(ptr:word, value:uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -instance address:StorageType { - function sload(ptr:word) -> address { - return Typedef.abs(sload_(ptr)):address; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:address) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : StorageType => instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) -> () { - StorageType.store(Typedef.rep(l), y); - } -} - -forall self fieldType offsetType. -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType. -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return storageRef(ptr); - } -} - -instance ():StorageSize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:StorageSize { - function size(x:Proxy(word)) -> word { - return 1; - } -} - -instance uint:StorageSize { - function size(x:Proxy(uint)) -> word { - return 1; - } -} - -instance address:StorageSize { - function size(x:Proxy(address)) -> word { - return 1; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:StorageSize -=> instance a:StorageSize { - function size(x:Proxy(a)) -> word { - return StorageSize.size(Proxy(b)); - } -} -*/ - -forall a b . a:StorageSize, b:StorageSize => instance (a,b):StorageSize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = StorageSize.size(Proxy:Proxy(a)); - let b_sz:word = StorageSize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -pragma no-patterson-condition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances -pragma no-coverage-condition MemberAccessProxy, LValueMemberAccess, RValueMemberAccess; - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = 0x100; // forge uses at least 1 storage slot - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - - assembly { - ptr := add(ptr, offsetSize) - } - return storageRef(ptr); // contract storage starts at 0 - } -} - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} - -/* -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} -*/ -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); - -forall member index . instance mapping(index, member):Typedef(word) { - function rep(x:mapping(index, member)) -> word { - match x { - | mapping(y) => return y; - } - } - function abs(x:word) -> mapping(index,member) { - return mapping(x); - } -} - - -// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays -forall index member . -instance mapping(index, member):StorageSize { - function size(x:Proxy(mapping(index, member))) -> word { - return 1; - } -} - -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); - -forall index member. index:Typedef(word) -=> instance IndexAccessProxy(storageRef(mapping(index,member)), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(mapping(index,member)), index, member)) -> storageRef(member) { - return storageRef(indexStorageSlot(x)); - } -} - -forall map index member . index:Typedef(word), member:StorageType, map:Typedef(word) -=> instance IndexAccessProxy(map, index, member):RValueMemberAccess(member) { - function memberAccess(x:IndexAccessProxy(map, index, member)) -> member { - let slot:word = indexStorageSlot(x); - return StorageType.sload(slot); - } -} - -forall index map member. map:Typedef(word), index:Typedef(word) => function indexStorageSlot(x:IndexAccessProxy(map, index, member)) -> word -//function indexStorageSlot(x) -{ - match x { - | IndexAccessProxy(map, i) => - let mapptr:word = Typedef.rep(map); - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} - -/* -forall index map member. map:Typedef(word), index:Typedef(word) -=> function indexedSlot(mapref : storageRef(mapping(index, member)), i: index) -> word -{ - match mapref { - | storageRef(mapptr) => - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} -*/ - -forall a b. a:RValueMemberAccess(b) => -function rval(x:a) -> b { - return RValueMemberAccess.memberAccess(x); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Invokable.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Invokable.solc deleted file mode 100644 index 1b041498..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Invokable.solc +++ /dev/null @@ -1,16 +0,0 @@ - -class self : invokable(args, ret) { - function invoke (s:self, a:args) -> ret; - } - - function id(x) { - return x ; - } - - data IdToken(a) = IdToken; - -instance IdToken(a) : invokable(a,a) { - function invoke(token: IdToken(a), a) -> a { - return id(a); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/KindTest.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/KindTest.solc deleted file mode 100644 index 9a4399f5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/KindTest.solc +++ /dev/null @@ -1,5 +0,0 @@ -data M = M; -function foo(x: M(Word)) {} - -data P(a) = P; -function foo2(x: P) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/ListModule.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/ListModule.solc deleted file mode 100644 index 59a14c10..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/ListModule.solc +++ /dev/null @@ -1,21 +0,0 @@ -contract ListModule { - data List(a) = Nil | Cons(a,List(a)); - data Bool = True | False; - - - function zipWith (f,xs,ys) { - match xs, ys { - | Nil, Nil => return Nil ; - | Cons(x1,xs1), Cons(y1,ys1) => - return Cons(f(x1,y1), zipWith(f,xs1,ys1)) ; - } - } - - function foldr(f, v, xs) { - match xs { - | Nil => return v; - | Cons(y,ys) => - return f(y, foldr(f,v,ys)) ; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Logic.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Logic.solc deleted file mode 100644 index 1a5372c1..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Logic.solc +++ /dev/null @@ -1,33 +0,0 @@ -contract Logic { - data Bool = True | False; - - function not (x) { - match x { - | True => return False ; - | False => return True ; - } - } - - function and(x, y) { - match x, y { - | False, _ => return False ; - | True , _ => return y ; - } - } - - function and1 (x, y) { - match x, y { - | False, False => return False ; - | True , False => return False; - | False ,True => return False; - | True, True => return True; - } - } - - function elim (f, g, x) { - match x { - | True => return f; - | False => return g; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/MatchCall.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/MatchCall.solc deleted file mode 100644 index b9b946a3..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/MatchCall.solc +++ /dev/null @@ -1,13 +0,0 @@ -data Bool = False | True; - -contract MatchCall { - function f() { - return True; - } - - function main() { - match f() { - | True => return 42; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory1.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory1.solc deleted file mode 100644 index 726784e4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory1.solc +++ /dev/null @@ -1,7 +0,0 @@ -data memory(a) = memory(word); - -function g() { - let x : memory(memory(word)); - let y : memory(word) = memory(1); - x = memory(0); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory2.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory2.solc deleted file mode 100644 index afb2e9e5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Memory2.solc +++ /dev/null @@ -1,5 +0,0 @@ -data Memory(a) = Memory(word); - -function g() { - let x : Memory(Memory(word)) = Memory(0); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Mutuals.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Mutuals.solc deleted file mode 100644 index 7729d1ba..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Mutuals.solc +++ /dev/null @@ -1,8 +0,0 @@ -contract Mutual { - function main () { - return f(); - } - function f () { - return 42; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/NegPair.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/NegPair.solc deleted file mode 100644 index 5946b9f2..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/NegPair.solc +++ /dev/null @@ -1,53 +0,0 @@ - -forall a . class a : Neg { - function neg(x:a) -> a; -} - -data B = F | T; - -instance B : Neg { - function neg (x : B) -> B { - match x { - | F => return T; - | T => return F; - } - } -} - -function fst (p) { - match p { - | (x,y) => return x; - } -} - -function snd(p) { - match p { - | (x,y) => return y; - } -} - - -forall a b . a : Neg, b : Neg => instance (a,b):Neg { - function neg(p : (a,b)) -> (a,b) { - return (Neg.neg (fst(p)), Neg.neg(snd (p))); - } -} - -contract NegPair { - - function bnot(x) { - match x { - | T => return F; - | F => return T; - } -} - - function fromB(b) { - match b { - | F => return 0; - | T => return 1; - } -} - - function main() { return fromB(fst(Neg.neg((F,T)))); } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Option.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Option.solc deleted file mode 100644 index 49a7119d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Option.solc +++ /dev/null @@ -1,11 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function join(mmx) { - match mmx { - | None => return None; - | Some(Some(x)) => return Some(x); - | Some(None) => return None; - } - } - } diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Pair.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Pair.solc deleted file mode 100644 index 836da609..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Pair.solc +++ /dev/null @@ -1,21 +0,0 @@ - function fst (x) { - match x { - | (a,_) => return a; - } - } - - forall a b . function snd(x : (a,b)) -> b { - match x { - | (_,b) => return b; - } - } - - function uncurry(f,x) { - match x { - | (a,b) => return f(a,b); - } - } - - function curry(f,x,y) { - return f((x,y)) ; - } diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch1.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch1.solc deleted file mode 100644 index 3f2a4636..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch1.solc +++ /dev/null @@ -1,6 +0,0 @@ -data Pair(a, b) = Pair(a, b); - -forall a . function foo(p: a) -> word { - let x: word = p; - return x; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch2.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch2.solc deleted file mode 100644 index 98395bb4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PairMatch2.solc +++ /dev/null @@ -1,8 +0,0 @@ - -forall a . function snd(p: (a, word)) -> a { - match p { - | (_, w) => return w; - } -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Peano.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Peano.solc deleted file mode 100644 index 276bd7f6..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Peano.solc +++ /dev/null @@ -1,12 +0,0 @@ -data Nat = Zero | Succ(Nat); - -function natInd (step,v,n) { - match n { - | Zero => return v ; - | Succ(m) => return step(m, natInd(step,v,m)); - } -} - -function add(n,m) { - return natInd (lam (x, acc) {return Succ(acc) ; }, m, n); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PeanoMatch.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/PeanoMatch.solc deleted file mode 100644 index 40a77944..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/PeanoMatch.solc +++ /dev/null @@ -1,9 +0,0 @@ -data Nat = Zero | Succ(Nat); - -function foo(n) { - match n { - | Zero => return Succ(Zero) ; - | Succ(Succ(x)) => return x; - | x => return Zero; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ref.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ref.solc deleted file mode 100644 index 8e402eaf..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Ref.solc +++ /dev/null @@ -1,16 +0,0 @@ -class ref : Ref(deref) { - function load (r : ref) -> deref; - function store (r : ref, d : deref) -> unit; -} - -data Memory(a) = new(a); - -instance Memory(a) : Ref(a) { - function load (r) { - match r { - | new(x) => return x; - } - } -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/RefDeref.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/RefDeref.solc deleted file mode 100644 index f096ffb5..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/RefDeref.solc +++ /dev/null @@ -1,12 +0,0 @@ -forall ref deref . class ref:Loadable (deref) { - function load (r : ref) -> deref; -} - -forall ref deref . class ref:Storable (deref) { - function store (r : ref, d : deref) -> (); -} - -// haskell style class constraints -forall ref deref . - ref : Loadable(deref) - , ref : Storable(ref) => class ref:Ref (deref) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SillyReturn.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/SillyReturn.solc deleted file mode 100644 index 0cc2b287..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SillyReturn.solc +++ /dev/null @@ -1,9 +0,0 @@ -data Nat = Zero | Succ(Nat); -data Bool = True | False; - -function even (n) -> Bool { - match n { - | Zero => return 1; return True; - | Succ(m) => return 0; return False; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleInvoke.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleInvoke.solc deleted file mode 100644 index 917d2e6a..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleInvoke.solc +++ /dev/null @@ -1,18 +0,0 @@ -function lambdaimpl1 (x) { - return x; -} -data LambdaTy0(a) = LambdaTy0; -class self : invokable (args, ret) { - function invoke (self : self, args : args) -> ret; -} -instance LambdaTy0(a) : invokable (a, a) { - forall a . function invoke (self : LambdaTy0(a), args : a) -> a { - return lambdaimpl1(args); - } -} -contract SimpleLambda { - function f () { - let n = LambdaTy0 ; - return invokable.invoke(n, 0); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleLambda.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleLambda.solc deleted file mode 100644 index 151e9c49..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SimpleLambda.solc +++ /dev/null @@ -1,22 +0,0 @@ -function addWord(x : word, y : word) -> word { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -contract SimpleLambda{ - function f (z) { - let n = lam (x,y) { - return addWord(x,addWord(y,1)); - } ; - let m = lam (x) { - return addWord (z,x) ; - } ; - return m(n(1,0)); - } - function main() { - return f(40); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SingleFun.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/SingleFun.solc deleted file mode 100644 index 50d3bddd..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/SingleFun.solc +++ /dev/null @@ -1,3 +0,0 @@ -function id (x) { - return x ; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/StructMembers.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/StructMembers.solc deleted file mode 100644 index cc9139b6..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/StructMembers.solc +++ /dev/null @@ -1,145 +0,0 @@ -/// Other used stdlib classes and types: -class self:Ref(deref) { - function load(x:self) -> deref; -} - -data Uint256 = Uint256(Word); -data Bool = True | False; -data Bytes32 = Bytes32(Word); -data Unit = Unit; - -data Proxy(t) = Proxy; -data Memory(x) = Memory(Word); - -/// Specific new stdlib classes and types: - -class self:StructMember(preceding, memberTy) {} -data StructMember(structType, fieldType) = StructMember; - -// "dead" is only here to compensate for non-relaxed coverage condition and -// incorrectly implemented Paterson condition -data MemberAccess(ty, field, dead) = MemberAccess(ty); - - -/// Usage Example / Proof of Concept: - -/* - struct S { - x:Uint256; - y:Bool; - z:Bytes32; - } -*/ - -data S = S(Pair(Uint256, Pair(Bool, Bytes32))); - -data Field_x = FieldX; // Selector type for "x" -data Field_y = FieldY; // Selector type for "y" -data Field_z = FieldZ; // Selector type for "z" - -// StructMember instances for field selectors: -instance StructMember(S, Field_x):StructMember(Unit, Uint256) {} -instance StructMember(S, Field_y):StructMember(Uint256, Bool) {} -instance StructMember(S, Field_z):StructMember(Pair(Uint256, Bool), Bytes32) {} - -/* Further compiler-internal builtin instances for use on stack (at least the stackref versions cannot be expressed in-language, - * but none of these rely on any layout other than the compiler-builtin stack layout, so we can handle these purely internally - * as "compiler magic"): - */ -/* - instance MemberAccess(S, Field_x):Ref(Uint256); - instance MemberAccess(stackref(S), Field_x):Ref(stackref(Uint256)); - instance MemberAccess(S, Field_y):Ref(Bool); - instance MemberAccess(stackref(S), Field_y):Ref(stackref(Bool)); - instance MemberAccess(S, Field_z):Ref(Bytes32); - instance MemberAccess(stackref(S), Field_z):Ref(stackref(Bytes32)); -*/ - - -/// Size of a type in memory -class self:MemorySize { - function memorySize(x:Proxy(self)) -> Word; -} - -/// Size of the struct member types in memory: -instance Unit:MemorySize { function memorySize(x) -> Word { return 0; } } -instance Uint256:MemorySize { function memorySize(x) -> Word { return 32; } } -instance Bool:MemorySize { function memorySize(x) -> Word { return 32; } } -instance Bytes32:MemorySize { function memorySize(x) -> Word { return 32; } } - -/// Memory size of pairs -instance Pair(a,b):MemorySize { - function memorySize(x) -> Word - { - let pa:Proxy(a); - let pb:Proxy(b); - let sz = memorySize(pa); - let szb = memorySize(pb); - assembly { sz := add(sz, szb) } // TODO: bounds check? - return sz; - } - -} - -/// Fragments of a generic memory implementation: -class self:MemoryType { - function loadFromMemory(p:Proxy(self), off:Word) -> self; -} - -instance Uint256:MemoryType { - function loadFromMemory(p:Proxy(Uint256), off:Word) -> Uint256 { - let v; - assembly { v := mload(off) } - return Uint256(v); - } -} - -instance (a:MemoryType) => Memory(a):Ref(a) { - function load(x) { - let p:Proxy(a); - match x { | Memory(off) => return loadFromMemory(p, off); } - } -} - -/// Crucial instance: member access to struct fields in memory: - -instance ( - StructMember(structType, fieldType):StructMember(precedingTuple, ty), - precedingTuple:MemorySize, - Memory(ty):Ref(ty) -) => MemberAccess(Memory(structType), fieldType, - // Needs ridiculous amounts of constructor applications due to incorrect implementation of the Paterson Condition - // Needs to mention "ty" due to non-relaxed Coverage Condition - Memory(ty) -):Ref(ty) -{ - function load(x) { - let ptr:Word; - match x { | MemberAccess(Memory(y)) => ptr = y; } - - let p:Proxy(precedingTuple); - let offset = memorySize(p); - - assembly { ptr := add(ptr, offset) } - - let tyPtr:Memory(ty) = Memory(ptr); - return load(tyPtr); - } -} - -function test() -{ - let x:Memory(S); - let memberAccess:MemberAccess(Memory(S), Field_x, - Memory(Uint256) // will become unnecessary - ); - memberAccess = MemberAccess(x); - let result = load(memberAccess); - /* - Eventually, I imagine ``let result = x.x;`` to merely desugar to - - let result = load(MemberAccess(x):MemberAccess(_, Field_x)); - - which is equivalent to the above. - */ -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Uncurry.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/Uncurry.solc deleted file mode 100644 index 8ce95c8b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/Uncurry.solc +++ /dev/null @@ -1,5 +0,0 @@ -function uncurry (f,p) { - match p { - | (x,y) => return f(x,y); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/add-moritz.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/add-moritz.solc deleted file mode 100644 index 672d879d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/add-moritz.solc +++ /dev/null @@ -1,70 +0,0 @@ -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -class self:Typedef(underlyingType) { - function rep(x:self) -> underlyingType; - function abs(x:underlyingType) -> self; -} - -forall a.class a : Add { - function add(x:a, y:a) -> a; -} - -data B = F | T; - - -instance B : Typedef(word) { - function rep(x) { - match x { - | F => return 0; - | T => return 1; - } - } - - function abs(x) { - match x { - | 0 => return F; - | 1 => return T; - } - } -} - -instance B : Add { - function add(x, y) { - match x { - | F => - match y { - | F => return F; - | T => return T; - } - - | T => - match y { - | F => return T; - | T => return F; - } - } - } -} - -function fun(a, b) { // -> c - match a, b { - | (a1, a2), (b1, b2) => return (Add.add(a1, b1), fun(a2, b2)); - } - -} - -contract Compose { - - function main() { - let res = fun ((T, T, F), (F, F, T)); - match res { - | (r1, r2, r3) => return Typedef.rep(r1); - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/another-subst.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/another-subst.solc deleted file mode 100644 index 37840ccd..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/another-subst.solc +++ /dev/null @@ -1,9 +0,0 @@ -forall a . class a: Foo {function foo(x:a) -> (); } - -forall a b . a : Foo, b : Foo => instance (a,b) : Foo { - function foo( p : (a,b) ) -> () { - match p { - | (pa, pb) => Foo.foo(pa); Foo.foo(pb); - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/app.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/app.solc deleted file mode 100644 index 6ec82d63..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/app.solc +++ /dev/null @@ -1,18 +0,0 @@ -function app () { - return lam (f, x) {return f(x);}; -} - -function id (x) { - return x; -} - -function foo() -> word { - let f = app(); - return f(id,0); -} - -contract C { - function main () -> word { - return foo(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/array.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/array.solc deleted file mode 100644 index f720c53b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/array.solc +++ /dev/null @@ -1,114 +0,0 @@ -pragma no-coverage-condition TAdd; - -data Zero; -data Succ(a); - -forall self res . class self:TAdd(res) {} -forall a . instance (Zero, a):TAdd(a) {} -forall a b c . (b, a):TAdd(c) => instance (Succ(b), a):TAdd(Succ(c)) {} - -forall lhs rhs . class lhs:Eq(rhs) {} -forall a . instance a:Eq(a) {} - -// this should work but doesnt: forall sizel sizer elem sizeout . (sizel, sizer):TAdd(sizeout) -// TODO: this panics during specialization -/* -forall sizel sizer elem sizeout pairSizelSizer . pairSizelSizer:Eq((sizel, sizer)), pairSizelSizer:TAdd(sizeout) => function concat(lhs:memory(array(sizel, elem)), rhs:memory(array(sizer, elem))) -> memory(array(sizeout, elem)) { - return memory(0) : memory(array(sizeout, elem)); // :D -} -*/ -data Itself(a) = ItselfRuntimeTag; - -data array(size, elem) = array; -data memory(a) = memory(word); - -forall self indexType elementType . class self:IndexAccessible (indexType, elementType){ - function set(self:self, ix:indexType, val:elementType) -> (); - function at(self:self, ix:indexType) -> elementType; -} - -forall self . class self:ToWord{ - function toWord(self:Itself(self)) -> word; -} - -instance Zero : ToWord { - function toWord(zero : Itself(Zero)) -> word { return 0; } -} - -forall prev . prev:ToWord => instance Succ(prev) : ToWord { - function toWord(self: Itself(Succ(prev))) -> word { - let returnVal : word = ToWord.toWord(ItselfRuntimeTag:Itself(prev)); - assembly { - returnVal := add(1, returnVal) - } - return returnVal; - } -} - -forall self . class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self); -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let val : word; - assembly { val := mload(ptr) } - return val; - } - function store(ptr:word, value:word) -> () { - assembly { mstore(ptr, value) } - } -} - -forall size elem . size : ToWord, elem:MemoryType => instance memory(array(size, elem)) : IndexAccessible(word, elem) { - function at(self : memory(array(size,elem)), index : word) -> elem { - let sizeValue = ToWord.toWord(ItselfRuntimeTag:Itself(size)); - - assembly { - if iszero(lt(index, sizeValue)) { - revert(0, 0) - } - } - - match self { - | memory(offset) => - let x = offset; // can't use this inside the assembly block :-( - assembly { - index := add(x, mul(32, index)) - } - return MemoryType.load(index); - } - } - - function set(self : memory(array(size,elem)), index : word, val : elem) -> () { - let sizeValue = ToWord.toWord(ItselfRuntimeTag:Itself(size)); - - assembly { - if iszero(lt(index, sizeValue)) { - revert(0, 0) - } - } - - match self { - | memory(offset) => - let x = offset; // can't use this inside the assembly block :-( - assembly { - index := add(x, mul(32, index)) - } - MemoryType.store(index, val); - } - } -} - - - -contract Array { - - function main() { - let arr : memory(array(Succ(Succ(Succ(Succ(Zero)))), word)) = memory(42); // = (1,2,3,4,5,6,7,8,9,10); - IndexAccessible.set(arr, 3, 33); - - return IndexAccessible.at(arr, 3); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/assembly.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/assembly.solc deleted file mode 100644 index 186f5237..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/assembly.solc +++ /dev/null @@ -1,17 +0,0 @@ -forall a . class a : Mem { - function size(x : a) -> word; -} - -instance word : Mem { - function size(x : word) -> word { - return 32; - } -} - -function foo () { - let ptr : word; - let size = Mem.size(0); - assembly { - ptr := add(32, size); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bal.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/bal.solc deleted file mode 100644 index f33dcf0e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bal.solc +++ /dev/null @@ -1,60 +0,0 @@ -data Proxy (a) = Proxy ; -data dict(member, index) = dict(word, Proxy(member), Proxy(index)) ; -data address = address(word) ; -data storage(a) = storage(word) ; - -data IndexAP (m, idx, member) = IndexAP(m, idx, Proxy(member)) ; - -function wal(ref: storage(dict(address, word)) , src : address, amt: word) { - let ip = IndexAP(ref, src, Proxy : Proxy(word)); - Assign.assign(LVA.acc(ip), amt); -} - - -/* Expected: - -ip : IndexAP(storage(dict(address, word)) , address, ?1) - -LVA.acc : forall self memberRefType. self:LVA(memberRefType) => self -> memberRefType - -instance IndexAP(storage(dict(index,member)), index, member):LVA(storage(member)) - - |- instance IndexAP(storage(map(address, word)), address, ?1) : LVA(storage(word))) where ?1 ~ word - -*/ - -/* Actual - -> Enter reduce() |- (?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) -> Reducing wanted constraints:(?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) using () -> After entailment:(?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) - () ->> Before eliminating equalities (?l4 : Assign (word), IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4)) ->> After eliminating equalities:(IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4), ?l4 : Assign (word)) ->>> Found instance for:IndexAP(storage(dict(address, word)), address, ?e4) : LVA (?l4) - ->>>Instance:?a5 ~ storage(?b5) => IndexAP(storage(dict(?c5, ?b5)), ?c5, ?b5) : LVA (?a5) !!! ->>>Subst:{?c5 +-> address, ?b5 +-> word, ?b5 +-> ?e4, ?l4 +-> ?a5} ??? - -b5 +-> e4 should really be b5 ~ e4 - -*/ -forall self memberRefType. -class self:LVA(memberRefType) { - function acc(x:self) -> memberRefType; -} - -forall index member. - instance IndexAP(storage(dict(index,member)), index, member):LVA(storage(member)) { - function acc(x:IndexAP(storage(dict(index,member)), index, member)) -> storage(member) { - return storage(30); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -forall a . instance storage(a):Assign(a) { - function assign(l:storage(a), y:a) -> () {} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-merge-case.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-merge-case.solc deleted file mode 100644 index fd45e002..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-merge-case.solc +++ /dev/null @@ -1,8 +0,0 @@ -// Pragmas to disable checks for specific classes -pragma no-patterson-condition TestClassP1, TestClassB1; -pragma no-coverage-condition TestClassC1; -//pragma no-bounded-variable-condition TestClassB1; -pragma no-bounded-variable-condition TestClassB1; - -// === Test Classes === -forall a . class a:TestClassP1 {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-minimal.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-minimal.solc deleted file mode 100644 index 85aa7e31..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-minimal.solc +++ /dev/null @@ -1,13 +0,0 @@ -// Minimal test for bound variable condition -// This SHOULD FAIL - variable 'bad' in context but not in instance head - -pragma no-patterson-condition TestBound; - -forall a . class a:TestBound {} -forall a b . class a:TestHelper(b) {} - -data TestType(x) = TestType; - -// Variable 'bad' appears in context but not in instance head -// Should fail bound variable check -forall x . bad:TestHelper(x) => instance TestType(x):TestBound {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-only-test.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-only-test.solc deleted file mode 100644 index 766704e4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-only-test.solc +++ /dev/null @@ -1,11 +0,0 @@ -// Test only bound variable check, disable Patterson -pragma no-patterson-condition TestBound; - -forall a . class a:TestBound {} -forall a b . class a:TestHelper(b) {} - -data TestType(x) = TestType; - -// Variable 'bad' appears in context but not in instance head -// Should fail bound variable check -forall x . bad:TestHelper(x) => instance TestType(x):TestBound {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-with-pragma.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-with-pragma.solc deleted file mode 100644 index 3f668786..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/bound-with-pragma.solc +++ /dev/null @@ -1,14 +0,0 @@ -// Same test but with pragma to disable bound variable check -// This SHOULD PASS - -pragma no-bounded-variable-condition TestBound; -pragma no-patterson-condition TestBound; // Also disable Patterson to avoid that error - -forall a . class a:TestBound {} -forall a b . class a:TestHelper(b) {} - -data TestType(x) = TestType; - -// Variable 'bad' appears in context but not in instance head -// But pragma disables the check, so should pass -forall x bad . bad:TestHelper(x) => instance TestType(x):TestBound {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/class-context.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/class-context.solc deleted file mode 100644 index 2b131ee9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/class-context.solc +++ /dev/null @@ -1,4 +0,0 @@ -forall self fieldType offsetType -. class self:StructField(fieldType, offsetType) { - function offsetSize(self) -> word; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-capture-only.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-capture-only.solc deleted file mode 100644 index 2f481c2d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-capture-only.solc +++ /dev/null @@ -1,8 +0,0 @@ -function test(x: word) { - return lam() { return x; }; -} - -function main() -> word { - let f = test(1); - return f(); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-bound-test.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-bound-test.solc deleted file mode 100644 index e5e3c30d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-bound-test.solc +++ /dev/null @@ -1,7 +0,0 @@ -function foo (b) { - let y:word; - let f = lam(x) { - if (b) { let z = 7; y = z; } else {x = 1;} - }; - return f(44); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-local.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-local.solc deleted file mode 100644 index 34761aba..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-local.solc +++ /dev/null @@ -1,13 +0,0 @@ -function test() -> word { - let f = lam (x: word) -> word { - let y = 42; - return y; - }; - return f(1); -} - -contract C { - function main() -> word { - return test(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-std.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-std.solc deleted file mode 100644 index e9d6f920..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var-std.solc +++ /dev/null @@ -1,14 +0,0 @@ -import std; - -contract Bug { - function main() -> word { - return makeClosure(42); - } - - function makeClosure(e : word) -> word { - let f = lam (x : word) { - return e + x; // Uses Add.add typeclass method - }; - return f(1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var.solc deleted file mode 100644 index edc5c7fc..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure-free-var.solc +++ /dev/null @@ -1,29 +0,0 @@ -function addW (l: word, r: word) -> word { - let rw : word; - assembly { - rw := add(l,r); - } - return rw; -} - -forall t . class t:Add { - function add(l: t, r: t) -> t; -} - -instance word:Add { - function add(l: word, r: word) -> word { return addW(l,r); } -} - -contract Bug { - function main() -> word { - return makeClosure(42); - } - - function makeClosure(e : word) -> word { - let f = lam (x : word) { - return Add.add(x,e); // this crashes - // return addW(e,x); // this works - }; - return f(1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure.solc deleted file mode 100644 index cb8f09af..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/closure.solc +++ /dev/null @@ -1,7 +0,0 @@ - function foo (z, k : (), a : word) { - let f = lam (x : word, y) { - k; - return primAddWord(a,primAddWord(y,z)); - }; - return f(0,1); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/comp.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/comp.solc deleted file mode 100644 index a49febd9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/comp.solc +++ /dev/null @@ -1,3 +0,0 @@ -function compose (f,g,x) { - return f(g(x)); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/comparisons.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/comparisons.solc deleted file mode 100644 index 1f1ae020..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/comparisons.solc +++ /dev/null @@ -1,14 +0,0 @@ -import std; -function f(x: word, y:word) -> bool { - return (!((x == y) - && (x != y) - && (x >= y) - && (x <= y) - || (x > y) - && (x < y) - )); -} - -contract Comparisons { - function main() -> bool { return f(0,1); } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/complexproxy.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/complexproxy.solc deleted file mode 100644 index d0c4af20..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/complexproxy.solc +++ /dev/null @@ -1,35 +0,0 @@ -data Proxy(a) = Proxy; - -function add(x:word, y: word) {return x;} - -class self:BaseMemoryType { - function memorySize(x:Proxy(self)) -> word; -} - - -instance word:BaseMemoryType { - function memorySize(x:Proxy(word)) -> word { - return 32; - } -} - -forall a b . a:BaseMemoryType, b:BaseMemoryType => - instance (a,b):BaseMemoryType { - - function memorySize(x) -> word { // not correct semantically, just for debugging - return add(BaseMemoryType.memorySize(Proxy:Proxy(a)), - // BaseMemoryType.memorySize(Proxy:Proxy(b)) - morefun(Proxy:Proxy(b)) - ); - } -} -// this should trigger a type error. -forall t. function morefun(p:Proxy(t)) -> word { - return BaseMemoryType.memorySize(Proxy:Proxy(t)); -} - -contract TestMemoryType { - function main() -> word { - return BaseMemoryType.memorySize(Proxy:Proxy( (word,word) )); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose0.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose0.solc deleted file mode 100644 index 10a70385..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose0.solc +++ /dev/null @@ -1,5 +0,0 @@ -forall a b c . function compose (f : (b) -> c,g : (a) -> b) -> ((a) -> c) { - return lam (x) { - return f(g(x)); - }; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose_desugared.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose_desugared.solc deleted file mode 100644 index da0929fa..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/compose_desugared.solc +++ /dev/null @@ -1,45 +0,0 @@ -forall a b c d e . d : invokable(b,c) - , e : invokable(a,b) - => function compose(f : d, g : e) -> t_closure1(a,b,c,d,e) { - return t_closure1(f,g); -} - -data t_closure1(a,b,c,d,e) = t_closure1(d,e); - -forall a b c d e . d : invokable(b,c), e : invokable(a,b) => - function lambda2(c : t_closure1(a,b,c,d,e), x : a) -> c { - match c { - | t_closure1(f, g) => - return invokable.invoke(f, invokable.invoke(g,x)); - } - } - -forall a b c d e . d : invokable(b,c) - , e : invokable(a,b) - => instance t_closure1(a,b,c,d,e) : invokable(a,c) { - function invoke(self : t_closure1(a,b,c,d,e), args : a) -> c { - return lambda2(self, args); - } -} - -data t_id3(a) = t_id3 ; - -forall a . function id (x : a) -> a { - return x; -} - -forall a . instance t_id3(a) : invokable(a,a) { - function invoke(self : t_id3(a), args : a) -> a { - match self { - | t_id3 => return id(args) ; - } - } -} - -contract Foo { - function main() -> word { - let f = compose(t_id3, t_id3); - return invokable.invoke(f, 0); - } -} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/const-array.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/const-array.solc deleted file mode 100644 index 3371a358..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/const-array.solc +++ /dev/null @@ -1,114 +0,0 @@ -pragma no-coverage-condition TAdd; - -data Zero; -data Succ(a); - -forall self res . class self:TAdd(res) {} -forall a . instance (Zero, a):TAdd(a) {} -forall a b c . (b, a):TAdd(c) => instance (Succ(b), a):TAdd(Succ(c)) {} - -forall lhs rhs . class lhs:Eq(rhs) {} -forall a . instance a:Eq(a) {} - -// this should work but doesnt: forall sizel sizer elem sizeout . (sizel, sizer):TAdd(sizeout) -forall sizel sizer elem sizeout pairSizelSizer . pairSizelSizer:Eq((sizel, sizer)), pairSizelSizer:TAdd(sizeout) => function concat(lhs:memory(array(sizel, elem)), rhs:memory(array(sizer, elem))) -> memory(array(sizeout, elem)) { - return memory(0) : memory(array(sizeout, elem)); // :D -} - -data Itself(a) = ItselfRuntimeTag; - -data array(size, elem) = array; -data memory(a) = memory(word); - -forall self indexType elementType . class self:IndexAccessible (indexType, elementType){ - function set(self:self, ix:indexType, val:elementType); - function at(self:self, ix:indexType) -> elementType; -} - -forall self . class self:ToWord{ - function toWord(self:Itself(self)) -> word; -} - -instance Zero : ToWord { - function toWord(zero) { return 0; } -} - -forall prev . prev:ToWord => instance Succ(prev) : ToWord { - function toWord(self: Itself(Succ(prev))) { - let returnVal : word = ToWord.toWord(ItselfRuntimeTag:Itself(prev)); - assembly { - returnVal := add(1, returnVal) - } - return returnVal; - } -} - -forall self . class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self); -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let val : word; - assembly { val := mload(ptr) } - return val; - } - function store(ptr:word, value:word) { - assembly { mstore(ptr, value) } - } -} - -forall size elem . size : ToWord, elem:MemoryType => instance memory(array(size, elem)) : IndexAccessible(word, elem) { - function at(self, index) -> elem { - let sizeValue = ToWord.toWord(ItselfRuntimeTag:Itself(size)); - // this should work but doesn't - // assembly { - // if iszero(lt(index, sizeValue)) { - // revert(0, 0) - // } - //} - - match self { - | memory(offset) => - let x = offset; // can't use this inside the assembly block :-( - assembly { - index := add(x, mul(32, index)) - } - return MemoryType.load(index); - } - } - - function set(self, index, val) { - let sizeValue = ToWord.toWord(ItselfRuntimeTag:Itself(size)); - - //assembly { - // if iszero(lt(index, sizeValue)) { - // revert(0, 0) - // } - //} - - match self { - | memory(offset) => - let x = offset; // can't use this inside the assembly block :-( - assembly { - index := add(x, mul(32, index)) - } - MemoryType.store(index, val); - } - } -} - - - -contract Array { - - function main() { - let arr : memory(array(Succ(Succ(Succ(Succ(Zero)))), word)) = memory(42); // = (1,2,3,4,5,6,7,8,9,10); - IndexAccessible.set(arr, 4, 33); - - // this (correctly) typechecks but doesn't specialize - let res = concat(arr, arr); // this typechecks - return IndexAccessible.at(arr, 4); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/const.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/const.solc deleted file mode 100644 index 6c6ed3b2..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/const.solc +++ /dev/null @@ -1,10 +0,0 @@ -function const() { - return lam (x, y) { return y ;} ; -} - -contract Foo { - function main () { - let f = const(); - return f(0,1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance-context.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance-context.solc deleted file mode 100644 index 8a6781cb..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance-context.solc +++ /dev/null @@ -1,26 +0,0 @@ - - -data memory(t) = memory(word); - -forall t . class t:ValueTy { - function rep(x:t) -> word; -} - -forall t . instance memory(t) : ValueTy { - function rep(x: memory(t)) -> word { - match x { - | memory(w) => return w; - } - } -} - -forall ref deref . class ref:Ref(deref) { - function store(loc: ref, value: deref) -> (); -} - -forall t . t : ValueTy => instance memory(t) : Ref(t) { - function store(loc: memory(t), value: t) -> () { - // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... - let vw = ValueTy.rep(value); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance.solc deleted file mode 100644 index 9ae0ccb4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constrained-instance.solc +++ /dev/null @@ -1,25 +0,0 @@ - -data memory(t) = memory(word); - -forall t . class t:ValueTy { - function rep(x:t) -> word; -} - -forall t . instance memory(t) : ValueTy { - function rep(x: memory(t)) -> word { - match x { - | memory(w) => return w; - } - } -} - -forall ref deref . class ref:Ref(deref) { - function store(loc: ref, value: deref) -> (); -} - -forall t . t : ValueTy => instance memory(t) : Ref(t) { - function store(loc: memory(t), value: t) -> () { - // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... - let vw = ValueTy.rep(value); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constructor-weak-args.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/constructor-weak-args.solc deleted file mode 100644 index 4f2ee34b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/constructor-weak-args.solc +++ /dev/null @@ -1,7 +0,0 @@ -forall ref deref . class ref:Loadable (deref) { - function load (r : ref) -> deref; -} - -forall t . t : Loadable(word) => function foo(v : t) -> word { - return Loadable.load(v); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs-inferred.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs-inferred.solc deleted file mode 100644 index be04beaf..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs-inferred.solc +++ /dev/null @@ -1,12 +0,0 @@ -function foo(x) { - return bar(x); -} -function bar(x) { - return foo(x); -} - -contract C { - function main() -> word { - return foo(1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs.solc deleted file mode 100644 index 512dab47..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/cyclical-defs.solc +++ /dev/null @@ -1,18 +0,0 @@ -function foo(x : word) -> word { - return bar(x); -} -function bar(x : word) -> word { - return foo(x); -} - -contract C { - function m(x : word) -> word { - return n(x); - } - function n(x : word) -> word { - return m(x); - } - function main() -> word { - return m(1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-inst.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-inst.solc deleted file mode 100644 index 0cd1b9e6..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-inst.solc +++ /dev/null @@ -1,19 +0,0 @@ -class self:Test { function f(x:self); } - -default instance a:Test { function f(x:self) {}} - -data memory(a) = memory(word); -data Proxy(a) = Proxy; - -instance memory(memory(word)):Test { function f(x:self) {}} - -forall a. -function f(p:Proxy(a)) { - let x:memory(a); - Test.f(x); -} - -function g() { - f(Proxy:Proxy(memory(memory(word)))); // needs to choose default instance in Test.f - f(Proxy:Proxy(memory(word))); // needs to choose concrete instance -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-missing.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-missing.solc deleted file mode 100644 index 59e710a2..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-missing.solc +++ /dev/null @@ -1,17 +0,0 @@ -class self:Test { function f(x:self); } - -data memory(a) = memory(word); -data Proxy(a) = Proxy; - -instance memory(memory(word)):Test { function f(x:self) {}} - -forall a. -function f(p:Proxy(a)) { - let x:memory(a); - Test.f(x); -} - -function g() { - f(Proxy:Proxy(memory(memory(word)))); // needs to choose default instance in Test.f - f(Proxy:Proxy(memory(word))); // needs to choose concrete instance -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-weak.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-weak.solc deleted file mode 100644 index c08f2b3b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/default-instance-weak.solc +++ /dev/null @@ -1,25 +0,0 @@ -class self:Test(weak) { function f(x:self) -> weak; } - -data memory(a) = memory(word); -data Proxy(a) = Proxy; -data Bool = True | False; -default instance a:Test(word) { function f(x:a) -> word { return 42; }} - -instance memory(memory(word)):Test(Bool) { function f(x:self) { return True; }} - -// If we choose the default instance to typecheck f, -// this will pass type-checking, since ``r`` is word. -// But: for a = memory(word), ``r`` will be ``bool`` and this is invalid! -forall a. -function f(p:Proxy(a)) { - let x:memory(a); - let r :word = Test.f(x); - assembly { - sstore(0, r) - } -} - -function g() { - f(Proxy:Proxy(memory(memory(word)))); // valid, since default instance is used - f(Proxy:Proxy(memory(word))); // PROBLEM: now we have a bool cross the assembly barrier -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/dispatch.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/dispatch.solc deleted file mode 100644 index e504f13c..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/dispatch.solc +++ /dev/null @@ -1,272 +0,0 @@ -pragma no-patterson-condition RunDispatch, MethodLevelCallvalueCheck, TopLevelCallvalueCheck; - -// --- Preliminaries --- - -data Bool = True | False; -data Proxy(a) = Proxy; - -// --- Core Data Types --- - -// A contract contains a tuple of methods and a single fallback -// TODO: implement receive() -data Contract(methods, fallback) = Contract(methods,fallback); - -// A method contains an implementation (fn) as well as it's name and type signature -data Method(name, args, rets, fn) = Method(name, args, rets, fn); - -// Contains the implementation for the fallback (fn) as well as it's type signature -data Fallback(args, rets, fn) = Fallback(args, rets, fn); - -// --- Method Selectors --- - -// For each method in a contract the compiler generates a unique type and -// produces a `Selector` instance for that type that returns the selector hash -forall nm . class nm:Selector { - function hash(prx: Proxy(nm)) -> word; -} - -// Method has a Selector if its name has a Selector -forall name args rets fn . name:Selector => instance Method(name,args,rets,fn):Selector { - function hash(prx: Proxy(Method(name,args,rets,fn))) -> word { - return Selector.hash(Proxy : Proxy(name)); - } -} - -// --- Method Execution --- - -// Describes how to execute a given method / fallback -forall ty callvalueCheckStatus . class ty:ExecMethod { - function exec(x: ty, pstatus : Proxy(callvalueCheckStatus)) -> (); -} - -// If fn matches the provided args/ret types, then we can execute any method -forall name args rets fn callvalueCheckStatus . fn:invokable(args,ret) => instance Method(name,Proxy(args),Proxy(rets),fn):ExecMethod { - function exec(m : Method(name,args,rets,fn), pstatus : Proxy(callvalueCheckStatus)) -> () { - match m { - | Method(nm,args,rets,fn) => - // check callvalue - MethodLevelCallvalueCheck.checkCallvalue(Proxy : Proxy(Method(name,args,rets,fn)), pstatus); - - // check we have enough calldata for the head of args - // abi decode args from calldata - // call fn with args - // abi encode rets to memory - // returndata copy encoded returns - // evm return - return (); - } - } -} - -// If fn matches the provided args/ret types, then we can execute any fallback -forall args rets fn callvalueCheckStatus . fn:invokable(args,ret) => instance Fallback(Proxy(args),Proxy(rets),fn):ExecMethod { - function exec(fb : Fallback(args,rets,fn), pstatus : Proxy (callvalueCheckStatus)) -> () { - match fb { - | Fallback(args, rets, fn) => - // check callvalue - MethodLevelCallvalueCheck.checkCallvalue(Proxy : Proxy(Fallback(args,rets,fn)), pstatus); - - // check we have enough calldata for the head of args - // abi decode args from calldata - // call fn with args - // abi encode rets to memory - // returndata copy encoded returns - // evm return - return (); - } - } -} - -// --- Method Dispatch --- - -// For a given tuple of methods this executes the method specified by the first four bytes of calldata -forall ty callvalueCheckStatus . class ty:RunDispatch { - function go(methods : ty, pstatus : Proxy(callvalueCheckStatus)) -> (); -} - -// We can dispatch to a single executable method with a known selector -// TODO: do we need this instance? -forall m callvalueCheckStatus . m:ExecMethod, m:Selector => instance m:RunDispatch { - function go(method : m, pstatus : Proxy(callvalueCheckStatus)) -> () { - match selector_matches(Proxy : Proxy(m)) { - | True => ExecMethod.exec(method, pstatus); - | False => return (); - } - } -} - -// We can dispatch to a tuple of executable methods with a known selector -forall n m callvalueCheckStatus . n:ExecMethod, n:Selector, m:ExecMethod, m:Selector => instance (n,m):RunDispatch { - function go(methods : (n,m), pstatus : Proxy(callvalueCheckStatus)) -> () { - match methods { - | (method_n, method_m) => - match selector_matches(Proxy : Proxy(n)) { - | True => ExecMethod.exec(method_n); - | False => match selector_matches(Proxy : Proxy(m)) { - | True => ExecMethod.exec(method_m, pstatus); - | False => return (); - } - } - } - } -} - -// Recursive instance -forall n m callvalueCheckStatus . n:ExecMethod, n:Selector, m:RunDispatch => instance (n,m):RunDispatch { - function go(methods : (n,m), pstatus : Proxy(callvalueCheckStatus)) -> () { - match methods { - | (method_n, rest) => - match selector_matches(Proxy : Proxy(n)) { - | True => ExecMethod.exec(method_n, pstatus); - | False => RunDispatch.go(rest, pstatus); - } - } - } -} - -// TODO: we only wanna do the calldataload once -// Given evidence of a name with a known selector, we can check if it matches the selector in the first four bytes of calldata -forall name . name:Selector => function selector_matches(prx : Proxy(name)) -> Bool { - let hash = Selector.hash(prx); - let res : word; - assembly { - let sel := shr(224, calldataload(0)); - res := eq(sel, hash); - } - match res { - | 0 => return False; - | _ => return True; - } -} - -// --- Callvalue Checks --- - -// If every method on a contract is non payable, we lift the callvalue check to run before method dispatch -// NonPayable instances should be generated by the compiler as part of desugaring -forall ty . class ty:NonPayable {} -forall ty . class ty:AllNonPayable {} -forall n m . n:NonPayable, m:AllNonPayable => instance (n,m):AllNonPayable {} - - -data CallvalueChecked; - -data CallvalueUnchecked; -forall ty . class ty:MethodsMustCheckCalldata {} -instance CallvalueUnchecked:MethodsMustCheckCalldata {} - -// If every method is non payable we run the callvalue check before method dispatch -forall ty ret . class ty:TopLevelCallvalueCheck(ret) { - function checkCallvalue(prx : Proxy(ty)) -> Proxy(ret); -} - -forall methods . default instance methods:TopLevelCallvalueCheck(CallvalueUnchecked) { - function checkCallvalue(prx : Proxy(methods)) -> Proxy(CallvalueUnchecked) { return Proxy : Proxy(CallvalueUnchecked); } -} - -forall methods . methods:AllNonPayable => instance methods:TopLevelCallvalueCheck(CallvalueChecked) { - function checkCallvalue(prx : Proxy(methods)) -> Proxy(CallvalueChecked) { - assembly { - if gt(callvalue(), 0) { - mstore(0,0x2) - revert(0,32); - } - } - return Proxy : Proxy(CallvalueChecked); - } -} - -// If only some methods are non payable, then we run the check during method execution -forall ty status . class ty:MethodLevelCallvalueCheck { - function checkCallvalue(pty : Proxy(ty), pstatus : Proxy(status)) -> (); -} - -forall method status . default instance method:MethodLevelCallvalueCheck { - function checkCallvalue(pty : Proxy(method), pstatus : Proxy(status)) -> () { } -} - -forall method status . method:NonPayable, status:MethodsMustCheckCalldata => instance method:MethodLevelCallvalueCheck { - function checkCallvalue(pty : Proxy(method), pstatus : Proxy(status)) -> (){ - assembly { - if gt(callvalue(), 0) { - mstore(0, 0x1); - revert(0, 32); - } - } - } -} - -// --- Contract Execution --- - -// Describes how to execute a given contract -forall c . class c:RunContract { - function exec(v : c) -> (); -} - -// If we have a dispatch for the contracts methods, and we know how to execute it's fallback, then we can define an entrypoint -forall methods fallback . methods:RunDispatch, fallback:ExecMethod => instance Contract(methods, fallback):RunContract { - function exec(c : Contract(methods, fallback)) -> () { - match c { - | Contract(ms, fb) => - // set free memory pointer to the output of memoryguard - // https://docs.soliditylang.org/en/v0.8.30/yul.html#memoryguard - // TODO: we will need to consider immutables here at some point... - // assembly { mstore(0x40, memoryguard(128)); } - - // if all methods are non payable then check callvalue - let callvalueChecked = TopLevelCallvalueCheck.checkCallvalue(Proxy : Proxy((fallback, methods))); - - // check that we have at least 4 bytes of calldata - let haveSelector : word; - assembly { - haveSelector := lt(3, calldatasize()); - } - - match haveSelector { - | 0 => assembly { revert(0,0); } - | _ => - // dispatch to method based on selector - RunDispatch.go(ms, callvalueChecked); - // run fallback if no methods matched - ExecMethod.exec(fb); - } - } - } -} - -// --- Manually Desugared Example --- - -// compiler generated - -function revert_handler() -> () { - assembly { revert(0,0) } -} - -data C_Add2_Selector = C_Add2_Selector; - -instance C_Add2_Selector:Selector { - function hash(prx: Proxy(C_Add2_Selector)) -> word { - // This would be keccak256("add2(uint256,uint256)") >> 224 - // Compiler computes this at compile time - return 0x29fcda33; // placeholder value - } -} - -// transform - -contract C { - function add2(x : word, y : word) -> word { - let ret : word; - assembly { ret := add(x,y) } - return ret; - } - - function main() -> word { - let c = Contract( - Method(C_Add2_Selector, Proxy : Proxy((word,word)), Proxy : Proxy(word), add2), - Fallback(Proxy : Proxy(()),Proxy : Proxy(()),revert_handler) - ); - - RunContract.exec(c); - return 0; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/duplicated-type-name.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/duplicated-type-name.solc deleted file mode 100644 index b752739d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/duplicated-type-name.solc +++ /dev/null @@ -1,6 +0,0 @@ -data Foo = Bar; -data Foo = Baz; - -function main() { - let x = Baz; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/empty-asm.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/empty-asm.solc deleted file mode 100644 index 81e6307d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/empty-asm.solc +++ /dev/null @@ -1,8 +0,0 @@ -function f(x : word) { - match x { - | 0 => - let ret : word; - assembly {} - return ret; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/foo-class.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/foo-class.solc deleted file mode 100644 index bb78a2ef..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/foo-class.solc +++ /dev/null @@ -1,4 +0,0 @@ -forall b self . -class self:Foo(b) { - function foo(x:self) -> b; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/if-examples.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/if-examples.solc deleted file mode 100644 index 59dc808b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/if-examples.solc +++ /dev/null @@ -1,46 +0,0 @@ -function toBool(x) { - match x { - | 0 => return false; - | _ => return true; - } -} - -function gt(x,y) { - let res : word; - assembly { - res := gt(x,y) - } - return toBool(res); -} - -function max(x,y) { - let res : word; - if (gt(x,y)) { - res = x; - } else { - res = y; - } - return res; -} - -function not(x:bool) -> bool { - if (x) { return false; } else { return true; } -} - -function foo () /* -> (word) -> bool */ { - return lam (x) { - if (gt(x,0)) { - return true; - } else { - return false; - } - }; -} - - -contract IfExamples { - function main() -> word { - let f = foo(); - return (if not(f(42)) then 0 else 1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/import-std.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/import-std.solc deleted file mode 100644 index 5e4a5771..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/import-std.solc +++ /dev/null @@ -1,7 +0,0 @@ -import std; - -contract Test { - function main() { - return Add.add(21, 21); - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/inc-closure.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/inc-closure.solc deleted file mode 100644 index be7b46da..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/inc-closure.solc +++ /dev/null @@ -1,17 +0,0 @@ -function inc(x : word) -> word { - let f = lam () { - let res : word ; - assembly { - res := add(x,1); - } - return res; - } ; - return f(); -} - -contract Foo { - - function main () -> word { - return inc(0); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/index-example.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/index-example.solc deleted file mode 100644 index 69cd808b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/index-example.solc +++ /dev/null @@ -1,74 +0,0 @@ -data storage(a) = storage(word); -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); // storage by default -// data mapRef(a) = mapRef(word); //ref to a map elem - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -forall a . instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) { - } -} - -forall self fieldType offsetType . class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(cxt, fieldSelector):StructField(fieldType, offsetType) - => instance MemberAccessProxy(cxt, fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(cxt, fieldSelector, offsetType)) -> storageRef(fieldType) { - return storageRef(0x100); - } -} - -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); -data IndexAccessProxy2(map, index, member) = IndexAccessProxy2(map, index, Proxy(member)); - -forall map index member. - instance IndexAccessProxy(storageRef(mapping(index,member)), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(map), index, member)) -> storageRef(member) { - return storageRef(0); - } -} - -data MintCtx = MintCtx; -data balances_sel = balances_sel; -instance StructField(MintCtx, balances_sel):StructField(mapping(word,word), ()) {} - - function mint(amount:word) { - let bal_prx = MemberAccessProxy(MintCtx, balances_sel); - let bal_ref = LValueMemberAccess.memberAccess(bal_prx); - - Assign.assign( - LValueMemberAccess.memberAccess( - IndexAccessProxy( - // bal_ref // this works, but inlining bal_ref leads to error - LValueMemberAccess.memberAccess(bal_prx) - , 0 - ) - ) - , amount - ) ; - - } diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error-invalid-member.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error-invalid-member.solc deleted file mode 100644 index ecd0fc2a..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error-invalid-member.solc +++ /dev/null @@ -1,11 +0,0 @@ -forall t . class t:CtFun { - function ct(x : t) -> ((t) -> t); -} - -instance word:CtFun { - function ct(x : word) -> ((word) -> word) { - return lam(y : bool) { - return x; - }; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error.solc deleted file mode 100644 index 0d6d22fc..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-closure-error.solc +++ /dev/null @@ -1,11 +0,0 @@ -forall t . class t:CtFun { - function ct(x : t) -> ((t) -> t); -} - -instance word:CtFun { - function ct(x : word) -> ((word) -> word) { - return lam(y : word) { - return x; - }; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-context-wrong-kind.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-context-wrong-kind.solc deleted file mode 100644 index 8487114f..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-context-wrong-kind.solc +++ /dev/null @@ -1,5 +0,0 @@ -forall a b . class a : Foo(b) {} - -forall a. class a:C {} - -forall t. t:Foo => instance (word,t):C {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-wrong-sig.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-wrong-sig.solc deleted file mode 100644 index ea5b8d7e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/instance-wrong-sig.solc +++ /dev/null @@ -1,15 +0,0 @@ -data uint256 = uint256(word); -data Proxy(a) = Proxy; -forall self . class self:ABIAttribs { - function headSize(ty:Proxy(self)) -> word; - function isStatic(ty:Proxy(self)) -> bool; -} - -instance ():ABIAttribs { - function headSize(ty : Proxy(uint256)) -> word { return 0; } - function isStatic(ty : Proxy(uint256)) -> bool { return true; } -} -instance uint256:ABIAttribs { - function headSize(ty : Proxy(uint256)) -> word { return 32; } - function isStatic(ty : Proxy(uint256)) -> bool { return true; } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/ixa.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/ixa.solc deleted file mode 100644 index e939bd15..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/ixa.solc +++ /dev/null @@ -1,143 +0,0 @@ -// --- preamble / duplicated std defs --- - -data Proxy(a) = Proxy; - -// dynamic arrays with a runtime size. cannot exist on stack so no data constructor (i.e. should be used in combination with memory / storage pointers). -data array(a); - -// a typed pointer to a location in memory -data memory(a) = memory(word); - -// word arithmetc -forall t . class t:Add { function add(l: t, r: t) -> t; } -forall t . class t:Mul { function mul(l: t, r: t) -> t; } -instance word:Add { - function add(l: word, r: word) -> word { - let rw : word; - assembly { - rw := add(l,r); - } - return rw; - } -} -instance word:Mul { - function mul(l: word, r: word) -> word { - let rw : word; - assembly { - rw := mul(l,r); - } - return rw; - } -} - -// --- MemoryType --- - -forall a . class a:MemoryType { - function load(loc : word) -> a; - function store(loc: word, val : a) -> (); - function size(prx : Proxy(a)) -> word; -} - -instance word:MemoryType { - function load(loc : word) -> word { - let ret : word; - assembly { ret := mload(loc) } - return ret; - } - - function store(loc : word, val : word) -> () { - assembly { mstore(loc,val) } - } - - function size(prx : Proxy(word)) -> word { - return 32; - } -} - -forall a . instance memory(array(a)):MemoryType { - function load(loc: word) -> memory(array(a)) { - let ret : word; - assembly { ret := mload(loc) } - return memory(ret); - } - - function store(loc : word, val : memory(array(a))) -> () { - match val { - | memory(ptr) => assembly { mstore(loc,ptr) } - } - } - - function size(prx : Proxy(memory(a))) -> word { - return 32; - } -} - -// --- Assignment --- - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l : lhs, r : rhs) -> (); -} - -instance memory(word):Assign(word) { - function assign(ptr : memory(word), val : word) -> () { - match ptr { - | memory(loc) => assembly { - mstore(loc, val) - } - } - } -} - -// --- Index Access --- - -forall col_idx val . class col_idx:RValueIdxAccess(val) { - function lookup(ci : col_idx) -> val; -} - -forall col_idx val . class col_idx:LValueIdxAccess(val) { - function lookup(ci : col_idx) -> val; -} - -forall a . a:MemoryType => instance (memory(array(a)), word):RValueIdxAccess(a) { - function lookup(col_idx : (memory(array(a)), word)) -> a { - let sz = MemoryType.size(Proxy : Proxy(a)); - match col_idx { - | (col, idx) => match col { - | memory(loc) => - return MemoryType.load(Add.add(loc, Mul.mul(idx, sz))); - } - } - } -} - -forall a . a:MemoryType => instance (memory(array(a)), word):LValueIdxAccess(memory(a)) { - function lookup(col_idx : (memory(array(a)), word)) -> memory(a) { - let sz = MemoryType.size(Proxy : Proxy(a)); - match col_idx { - | (col, idx) => match col { - | memory(loc) => return memory(Add.add(loc, Mul.mul(idx, sz))); - } - } - } -} - -// --- Examples --- - -function main() -> () { - let x : memory(array(memory(array(word)))) = memory(0); - let y : word = 0; - let z : memory(array(word)) = memory(0); - - // y = z[0] - y = RValueIdxAccess.lookup((z, 0)); - - //y = x[0][1] - y = RValueIdxAccess.lookup((RValueIdxAccess.lookup((x, 0)), 1)); - - //x[2][3] = x[5][4] - Assign.assign( - // TODO: R or L for the x[2] lookup? - LValueIdxAccess.lookup((RValueIdxAccess.lookup((x, 2)), 3)), - RValueIdxAccess.lookup((RValueIdxAccess.lookup((x, 5)), 4)) - ); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/join.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/join.solc deleted file mode 100644 index 3a20daa9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/join.solc +++ /dev/null @@ -1,24 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - data Bool = False | True; - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - let result = None; - match mmx { - | Some(Some(x)) => result = Some(x); - | None => result = None; - } - return result; - } - - function main() { - return maybe(0, join(Some(Some(0)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/joinErr.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/joinErr.solc deleted file mode 100644 index 4c6dac2e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/joinErr.solc +++ /dev/null @@ -1,25 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - data Bool = False | True; - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - let result = None; - match mmx { - | Some(Some(x)) => result = Some(x); - | None => result = None; - } - return result; - } - - - function main() { - return maybe(0, join(Some(Some(False)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/listeq.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/listeq.solc deleted file mode 100644 index 1e8250e9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/listeq.solc +++ /dev/null @@ -1,10 +0,0 @@ -data List(a) = Nil | Cons(a,List(a)); -data Bool = False | True; - -forall a . class a : Eq { - function eq (x : a, y : a) -> Bool ; -} - -function foo () { - return Eq.eq(Nil, Nil); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/listid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/listid.solc deleted file mode 100644 index 1f02717e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/listid.solc +++ /dev/null @@ -1,12 +0,0 @@ -data List(a) = Nil | Cons(a, List(a)); - -forall a . function id(x : a) -> a { - return x; -} - -function listid(xs) { - match xs { - | Nil => return Nil ; - | Cons(x,xs) => return Cons(id(x), listid(xs)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/mainproxy.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/mainproxy.solc deleted file mode 100644 index 77a12b58..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/mainproxy.solc +++ /dev/null @@ -1,22 +0,0 @@ -data Proxy(a) = Proxy; - -class self:BaseMemoryType { - function memorySize(x:Proxy(self)) -> word; -} - - -instance word:BaseMemoryType { - function memorySize(x:Proxy(self)) -> word { - return 32; - } -} - - -function morefun(p:Proxy(t)) -> word { return BaseMemoryType.memorySize(Proxy:Proxy(t)); -} - -contract TestMemoryType { - function main() -> word { - return morefun(Proxy:Proxy(word)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/match-yul.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/match-yul.solc deleted file mode 100644 index 92989972..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/match-yul.solc +++ /dev/null @@ -1,15 +0,0 @@ -data Wrapper = Wrapper(word); -contract C { - function main() -> word { - return foo(Wrapper(1)); - } - function foo(w:Wrapper) -> word { - let result : word; - match w { - | Wrapper(ptr) => - //let ptr2 : word = ptr; - assembly { result := calldataload(ptr) } - } - return result; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/memory.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/memory.solc deleted file mode 100644 index 9ed30b4b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/memory.solc +++ /dev/null @@ -1,10 +0,0 @@ -data Memory(t) = Memory(word); -data Bytes = Bytes; - -function get_bytes() -> Memory(Bytes) { - let ptr : word; - assembly { - ptr := mload(0x40) - } - return Memory(ptr); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/missing-instance.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/missing-instance.solc deleted file mode 100644 index 8d31f598..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/missing-instance.solc +++ /dev/null @@ -1,26 +0,0 @@ -// Note: this class has no instances! -forall abs rep . class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - -forall self . class self:MemoryType { - function load(ptr:word) -> self; -} - -instance word:MemoryType { - function load(ptr:word) -> word { - return Typedef.abs(MemoryType.load(ptr) : word); - // `abs` does not make sense here, but it triggers the bug: - // the typechecker should complain about missing instance here - } -} - -contract C { - function main() -> word { - let ptr = 0; - // if we inline the let below into return then another bug occurs: main is typed as forall a. () -> a - // let w:word = MemoryType.load(0); - return MemoryType.load(0); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/modifier.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/modifier.solc deleted file mode 100644 index 96e56b46..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/modifier.solc +++ /dev/null @@ -1,28 +0,0 @@ -contract C { - function modifier(f) { - return lam (x, y) { - // before solidity placeholder - let result = f(x,y); // Solidity's placeholder: _; - // after solidity placeholder - return result; - }; - } - - function add(x: word, y:word) -> word { - let r : word; - assembly { - r := add(x, y) - } - return r; - } - - function main() { - //function g(x, y) modifier(x,y) { - // return add(x,y); - //} - let g = modifier(lam (x, y) { - return add(x, y); - }); - return g(2,1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/morefun.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/morefun.solc deleted file mode 100644 index fbcd6058..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/morefun.solc +++ /dev/null @@ -1,9 +0,0 @@ -data Proxy(a) = Proxy; - -forall a . class a:C { - function fun(p:Proxy(a)) -> word; -} - -forall t . t : C => function morefun(p:Proxy(t)) -> word { - return C.fun(Proxy:Proxy(t)); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/nano-desugared.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/nano-desugared.solc deleted file mode 100644 index a0cfbfc6..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/nano-desugared.solc +++ /dev/null @@ -1,443 +0,0 @@ -function addW (x : word, y : word) { - let res : word ; - assembly { res := add(x, y) - } - return res; -} -function subW (x : word, y : word) { - let res : word ; - assembly { res := sub(x, y) - } - return res; -} -function addU (x : uint, y : uint) -> uint { - let res : word ; - let xw : word = Num.toWord(x) ; - let yw : word = Num.toWord(y) ; - assembly { res := add(xw, yw) - } - return uint(res); -} -function hash1 (x : word) -> word { - let result : word = 0 ; - assembly { mstore(0, x) - result := keccak256(0, 32) - } - return result; -} -function hash2 (x : word, y : word) -> word { - let result : word = 0 ; - assembly { mstore(0, x) - mstore(32, y) - result := keccak256(0, 64) - } - return result; -} -data Bool = False | True ; -function not (b : Bool) -> Bool { - match (b) { - | False => - return True; - | True => - return False; - } -} -function or (x : Bool, y : Bool) -> Bool { - match (x) { - | False => - return y; - | True => - return True; - } -} -function fromBool (b) { - match (b) { - | False => - return 0; - | True => - return 1; - } -} -function toBool (x : word) { - match (x) { - | 0 => - return False; - | _ => - return True; - } -} -forall a . class a : Num { - function toWord (x : a) -> word; - function fromWord (x : word) -> a; - function add (x : a, y : a) -> a; - function sub (x : a, y : a) -> a; - function eq (x : a, y : a) -> Bool; - function gt (x : a, y : a) -> Bool; -} -instance word : Num { - function toWord (x : word) -> word { - return x; - } - function fromWord (x : word) -> word { - return x; - } - function add (x : word, y : word) -> word { - return addW(x, y); - } - function sub (x : word, y : word) -> word { - return addW(x, y); - } - function eq (x : word, y : word) -> Bool { - let res : word ; - assembly { res := eq(x, y) - } - return toBool(res); - } - function gt (x : word, y : word) -> Bool { - let res : word ; - assembly { res := gt(x, y) - } - return toBool(res); - } -} -forall a . a : Num => function ge (x : a, y : a) -> Bool { - return or(Num.gt(x, y), Num.eq(x, y)); -} -data uint = uint(word) ; -instance uint : Num { - function toWord (x : uint) -> word { - match (x) { - | uint(y) => - return y; - } - } - - function fromWord (x : word) -> uint { - return uint(x); - } - function add (x : uint, y : uint) -> uint { - return uint(addW(Num.toWord(x), Num.toWord(y))); - } - function sub (x : uint, y : uint) -> uint { - return uint(subW(Num.toWord(x), Num.toWord(y))); - } - function eq (x : uint, y : uint) -> Bool { - return Num.eq(Num.toWord(x), Num.toWord(y)); - } - function gt (x : uint, y : uint) -> Bool { - return Num.gt(Num.toWord(x), Num.toWord(y)); - } -} -forall abs rep . class abs : Typedef (rep) { - function rep (x : abs) -> rep; - function abs (x : rep) -> abs; -} -instance word : Typedef (word) { - function rep (x : word) -> word { - return x; - } - function abs (x : word) -> word { - return x; - } -} -instance uint : Typedef (word) { - function rep (x : uint) -> word { - match (x) { - | uint(y) => - return y; - } - } - function abs (x : word) -> uint { - return uint(x); - } -} -data address = address(word) ; -instance address : Typedef (word) { - function rep (x : address) -> word { - match (x) { - | address(y) => - return y; - } - } - function abs (x : word) -> address { - return address(x); - } -} -data storage (a) = storage(word) ; -data ContractStorage (cxt) = ContractStorage(cxt) ; -data storageRef (a) = storageRef(word) ; -data Proxy (a) = Proxy ; -data mapping (member, index) = mapping(word, Proxy(member), Proxy(index)) ; -data mapRef (a) = mapRef(word) ; -forall a . instance storage(a) : Typedef (word) { - function rep (x : storage(a)) -> word { - match (x) { - | storage(y) => - return y; - } - } - function abs (x : word) -> storage(a) { - return storage(x); - } -} -forall a . instance storageRef(a) : Typedef (word) { - function rep (x : storageRef(a)) -> word { - match (x) { - | storageRef(y) => - return y; - } - } - function abs (x : word) -> storageRef(a) { - return storageRef(x); - } -} -forall lhs rhs . class lhs : Assign (rhs) { - function assign (l : lhs, r : rhs) -> (); -} -data ref (a) = ref(a) ; -forall a . instance ref(a) : Assign (a) { - function assign (l : ref(a), r : a) -> () { - return (); - } -} -forall self . class self : StorageType { - function sload (ptr : word) -> self; - function store (ptr : word, value : self) -> (); -} -forall self . class self : StorageSize { - function size (x : Proxy(self)) -> word; -} -function sload_ (x : word) -> word { - let res : word ; - assembly { res := sload(x) - } - return res; -} -function sstore_ (a : word, v : word) { - assembly { sstore(a, v) - } -} -instance word : StorageType { - function sload (ptr : word) -> word { - let r : word ; - assembly { r := sload(ptr) - } - return r; - } - function store (ptr : word, value : word) -> () { - assembly { sstore(ptr, value) - } - } -} -instance uint : StorageType { - function sload (ptr : word) -> uint { - return Typedef.abs(sload_(ptr)) : uint; - } - function store (ptr : word, value : uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} -instance address : StorageType { - function sload (ptr : word) -> address { - return Typedef.abs(sload_(ptr)) : address; - } - function store (ptr : word, value : address) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} -forall a . a : StorageType => instance storageRef(a) : Assign (a) { - function assign (l : storageRef(a), y : a) -> () { - StorageType.store(Typedef.rep(l), y); - } -} -forall self fieldType offsetType . class self : StructField (fieldType, offsetType) { -} -data StructField (structType, fieldSelector) = StructField(structType) ; -data MemberAccessProxy (a, field, offset) = MemberAccessProxy(a, field) ; -forall a field offset . function memberAccessD1 (x : MemberAccessProxy(a, field, offset)) -> a { - match (x) { - | MemberAccessProxy(y, z) => - return y; - } -} -forall self memberRefType . class self : LValueMemberAccess (memberRefType) { - function memberAccess (x : self) -> memberRefType; -} -forall self memberValueType . class self : RValueMemberAccess (memberValueType) { - function memberAccess (x : self) -> memberValueType; -} -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector) : StructField (fieldType, offsetType), offsetType : StorageSize => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType) : LValueMemberAccess (storageRef(fieldType)) { - function memberAccess (x : MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr : word = Typedef.rep(memberAccessD1(x)) ; - let size : word = StorageSize.size(Proxy : Proxy(offsetType)) ; - assembly { ptr := add(ptr, size) - } - return storageRef(ptr); - } -} -instance () : StorageSize { - function size (x : Proxy(())) -> word { - return 0; - } -} -instance word : StorageSize { - function size (x : Proxy(word)) -> word { - return 1; - } -} -instance uint : StorageSize { - function size (x : Proxy(uint)) -> word { - return 1; - } -} -instance address : StorageSize { - function size (x : Proxy(address)) -> word { - return 1; - } -} -forall a b . a : StorageSize, b : StorageSize => instance (a, b) : StorageSize { - function size (x : Proxy((a, b))) -> word { - let a_sz : word = StorageSize.size(Proxy : Proxy(a)) ; - let b_sz : word = StorageSize.size(Proxy : Proxy(b)) ; - assembly { a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} -pragma no-patterson-condition RValueMemberAccess ; -pragma no-coverage-condition MemberAccessProxy, LValueMemberAccess, RValueMemberAccess ; -forall cxt fieldSelector fieldType offsetType . StructField(ContractStorage(cxt), fieldSelector) : StructField (fieldType, offsetType), offsetType : StorageSize => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType) : LValueMemberAccess (storageRef(fieldType)) { - function memberAccess (x : MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr : word = 256 ; - let offsetSize : word = StorageSize.size(Proxy : Proxy(offsetType)) ; - assembly { ptr := add(ptr, offsetSize) - } - return storageRef(ptr); - } -} -forall cxt fieldSelector fieldType offsetType . StructField(ContractStorage(cxt), fieldSelector) : StructField (fieldType, offsetType), fieldType : StorageType, offsetType : StorageSize => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType) : RValueMemberAccess (fieldType) { - function memberAccess (x : MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr : word = 256 ; - let offsetSize : word = StorageSize.size(Proxy : Proxy(offsetType)) ; - return StorageType.sload(addW(ptr, offsetSize)) : fieldType; - } -} -data mapping (index, member) = mapping(word) ; -forall member index . instance mapping(index, member) : Typedef (word) { - function rep (x : mapping(index, member)) -> word { - match (x) { - | mapping(y) => - return y; - } - } - function abs (x : word) -> mapping(index, member) { - return mapping(x); - } -} -forall index member . instance mapping(index, member) : StorageSize { - function size (x : Proxy(mapping(index, member))) -> word { - return 1; - } -} -data IndexAccessProxy (map, index, member) = IndexAccessProxy(map, index) ; -forall index member . index : Typedef (word) => instance IndexAccessProxy(storageRef(mapping(index, member)), index, member) : LValueMemberAccess (storageRef(member)) { - function memberAccess (x : IndexAccessProxy(storageRef(mapping(index, member)), index, member)) -> storageRef(member) { - return storageRef(indexStorageSlot(x)); - } -} -forall map index member . index : Typedef (word), member : StorageType, map : Typedef (word) => instance IndexAccessProxy(map, index, member) : RValueMemberAccess (member) { - function memberAccess (x : IndexAccessProxy(map, index, member)) -> member { - let slot : word = indexStorageSlot(x) ; - return StorageType.sload(slot); - } -} -forall index map member . map : Typedef (word), index : Typedef (word) => function indexStorageSlot (x : IndexAccessProxy(map, index, member)) -> word { - match (x) { - | IndexAccessProxy(map, i) => - let mapptr : word = Typedef.rep(map) ; - let rawidx : word = Typedef.rep(i) ; - let loc : word = hash2(mapptr, rawidx) ; - return loc; - } -} -forall a b . a : RValueMemberAccess (b) => function rval (x : a) -> b { - return RValueMemberAccess.memberAccess(x); -} -function caller () -> address { - let res : word ; - assembly { res := caller() - } - return address(res); -} -function require1fail () { - let res : word ; - assembly { mstore(0, 2320231852978620534530211544385868) - revert(0, 32) - } - return (); -} -function require1 (cond : Bool) { - match (cond) { - | False => - return require1fail(); - | True => - return (); - } -} -function nop () -> () { - return (); -} -data UintCxt = UintCxt ; -data reserved_sel = reserved_sel ; -instance StructField(ContractStorage(UintCxt), reserved_sel) : StructField (word, ()) { -} -data msg_sender_sel = msg_sender_sel ; -instance StructField(ContractStorage(UintCxt), msg_sender_sel) : StructField (address, (word, ())) { -} -data owner_sel = owner_sel ; -instance StructField(ContractStorage(UintCxt), owner_sel) : StructField (address, (word, (address, ()))) { -} -data decimals_sel = decimals_sel ; -instance StructField(ContractStorage(UintCxt), decimals_sel) : StructField (uint, (word, (address, (address, ())))) { -} -data totalSupply_sel = totalSupply_sel ; -instance StructField(ContractStorage(UintCxt), totalSupply_sel) : StructField (uint, (word, (address, (address, (uint, ()))))) { -} -data balances_sel = balances_sel ; -instance StructField(ContractStorage(UintCxt), balances_sel) : StructField (mapping(address, uint), (word, (address, (address, (uint, (uint, ())))))) { -} -contract Uint { - function mint (amount : uint) { - Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)))), Num.add(rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)))), amount)); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), totalSupply_sel)), Num.add(rval(MemberAccessProxy(ContractStorage(UintCxt), totalSupply_sel)), amount)); - } - function transferFrom (src : address, dst : address, amt : uint) -> Bool { - require1(ge(rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), src)), amt)); - withdraw(src, amt); - deposit(dst, amt); - return True; - } - function withdraw (src : address, amt : uint) { - Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), src)), Num.sub(rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), src)), amt) : uint); - } - function deposit (dst : address, amt : uint) { - Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), dst)), Num.add(rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), dst)), amt) : uint); - } - function init () { - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)), address(81985529216486895)); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), msg_sender_sel)), caller()); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), decimals_sel)), Num.fromWord(18)); - } - function main () -> uint { - init(); - mint(uint(1000)); - mint(uint(1000)); - let amt = uint(1) ; - let src : address = rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)) ; - transferFrom(rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), msg_sender_sel)), uint(42)); - require1(True) : (); - return rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), msg_sender_sel)))):uint; - } -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/nid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/nid.solc deleted file mode 100644 index d40a934e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/nid.solc +++ /dev/null @@ -1,8 +0,0 @@ -function nid() { - return id; -} -function id (x) { - return x; -} - - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/noclosure.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/noclosure.solc deleted file mode 100644 index 8b51fc4d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/noclosure.solc +++ /dev/null @@ -1,6 +0,0 @@ -function foo (z) { - let f = lam (x : word, y) { - return primAddWord(x,primAddWord(y,1)); - }; - return primAddWord(f(0,1),z); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/noconstr.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/noconstr.solc deleted file mode 100644 index ef79dd9c..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/noconstr.solc +++ /dev/null @@ -1,17 +0,0 @@ -class a : Foo { - function foo (x : a) -> word; -} - -// here the constraint a : Foo is -// defered to outer scope where the -// error should be detected. - -function bla (x : a) -> word { - return Foo.foo(x); -} - -contract Test { - function main() { - return bla(1); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/notif.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/notif.solc deleted file mode 100644 index 3b91881f..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/notif.solc +++ /dev/null @@ -1,14 +0,0 @@ -function not(x){ - if (x) { - return false ; - } else { - return true ; - } -} - -function not2(x) { - if (x) { - return false ; - } - return true; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/option2.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/option2.solc deleted file mode 100644 index 76c9af45..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/option2.solc +++ /dev/null @@ -1,35 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - match mmx { - | None => return None; - | Some(None) => return None; - | Some(Some(x)) => return Some(x); - } - } - - function join2(mmx) { - match mmx { - | Some(m) => match m { - | None => return None; - | Some(x) => return Some(x); - } - | _ => return None; - } - } - - function main() { - // return maybe(0, join(Some(Some(42)))); - return 42; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/overlapping-heads.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/overlapping-heads.solc deleted file mode 100644 index 152ad54f..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/overlapping-heads.solc +++ /dev/null @@ -1,15 +0,0 @@ -forall a b . class a : Foo(b) { - function foo (x : a, y : word) -> b; -} - -instance () : Foo (()) { - function foo (x : (), y : word) -> () { - return (); - } -} - -forall a . instance a : Foo (()) { - function foo (x : a, y : word) -> () { - return (); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/patterson-bug.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/patterson-bug.solc deleted file mode 100644 index 67aedfec..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/patterson-bug.solc +++ /dev/null @@ -1,78 +0,0 @@ -data storage(a) = storage(word); -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); // storage by default -// data mapRef(a) = mapRef(word); //ref to a map elem - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -forall a . instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) -> () { - - } -} - -forall self fieldType offsetType . class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -pragma no-coverage-condition LValueMemberAccess; - -forall cxt fieldSelector fieldType offsetType - . StructField(cxt, fieldSelector):StructField(fieldType, offsetType) - => instance MemberAccessProxy(cxt, fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(cxt, fieldSelector, offsetType)) -> storageRef(fieldType) { - return storageRef(0x100); - } -} - -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); -data IndexAccessProxy2(map, index, member) = IndexAccessProxy2(map, index, Proxy(member)); - -forall map index member. - instance IndexAccessProxy(storageRef(mapping(index,member)), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(map), index, member)) -> storageRef(member) { - return storageRef(0); - } -} - -data MintCtx = MintCtx; -data balances_sel = balances_sel; -instance StructField(MintCtx, balances_sel):StructField(mapping(word,word), ()) {} - - function mint(amount:word) { - let bal_prx = MemberAccessProxy(MintCtx, balances_sel); - let bal_ref = LValueMemberAccess.memberAccess(bal_prx); - - Assign.assign( - LValueMemberAccess.memberAccess( - IndexAccessProxy( - // bal_ref // this works, but inlining bal_ref leads to error - LValueMemberAccess.memberAccess(bal_prx) - , 0 - ) - ) - , amount - ) ; - - } - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/polymatch-error.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/polymatch-error.solc deleted file mode 100644 index 1c27cbda..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/polymatch-error.solc +++ /dev/null @@ -1,12 +0,0 @@ -forall a b . function fst(p: (a, b)) -> a { - match p { - | (a, _) => return a; - } -} -contract TestUnitMatch { - function main() -> () { - match ((), ()) { - | x => return fst(x); - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_base.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_base.solc deleted file mode 100644 index 98d66cfb..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_base.solc +++ /dev/null @@ -1,47 +0,0 @@ -// Test base file for pragma merging functionality -// This file contains violations of all three condition types with pragmas to disable checks - -// Pragmas to disable checks for specific classes -pragma no-patterson-condition TestClassP1, TestClassB1, TestClassP3, TestClassB3; -pragma no-coverage-condition TestClassC1, TestClassP3; -pragma no-bounded-variable-condition TestClassB1, TestClassB3; - -// --- Test Classes --- - -forall a . class a:TestClassP1 {} -forall a . class a:TestClassP2 {} -forall a b . class a:TestClassP3(b) {} - -forall a b . class a:TestClassC1(b) {} -forall a b c . class a:TestClassC2(b,c) {} - -forall a b . class a:TestClassB1(b) {} -forall a b . class a:TestClassB2(b) {} -forall a . class a:TestClassB3 {} - -// --- Data Types --- - -data TestType1(x) = TestType1; -data TestType2 = TestType2; - -// Fails Patterson: context constraint not smaller then head -forall U . (U,word):TestClassP1 => instance U:TestClassP1 {} - -// Patterson OK: No context predicates -instance TestType2:TestClassP2 {} - -// --- Coverage Condition --- - -// Fails Coverage: Variable 'a' only appears in weak position (parameter to TestClassC1) -forall a b . instance TestType1(b):TestClassC1(a) {} - -// Coverage OK: All variables in strong positions -instance TestType2:TestClassC2(TestType2, TestType2) {} - -// === Bound Variable Violations === - -// Fails Bound Variable & Patterson: Variable 'c' appears in context but not in instance head -forall a c . c:TestClassB2(a) => instance TestType1(a):TestClassB1(a) {} - -// Bound Variable OK: Simple instance without context -instance TestType1(TestType2):TestClassB2(TestType2) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_coverage.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_coverage.solc deleted file mode 100644 index 2576051f..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_coverage.solc +++ /dev/null @@ -1,10 +0,0 @@ -// Negative test for pragma merging - should fail -import pragma_merge_base; - -forall a . class a:TestFailClass {} - -data FailType(x) = FailType; - -// should fail because TestFailCoverage doesn't have no-coverage-condition -forall a b . class a:TestFailCoverage(b) {} -forall x y . instance FailType(x):TestFailCoverage(y) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_patterson.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_patterson.solc deleted file mode 100644 index 89637290..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_fail_patterson.solc +++ /dev/null @@ -1,11 +0,0 @@ -// This file should FAIL compilation to demonstrate that checks are working when the imported file contains violations - -import pragma_merge_base; - - -// --- Patterson Violation --- - -forall a . class a:TestFailClass {} - -// Should fail because TestFailClass doesn't have no-patterson-condition -forall U . U:TestClassP1, U:TestClassP2, U:TestClassP3 => instance U:TestFailClass {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_import.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_import.solc deleted file mode 100644 index 31823792..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_import.solc +++ /dev/null @@ -1,26 +0,0 @@ -// Test import file for pragma merging functionality -// This file imports pragma_merge_base and adds its own violations with pragmas -// Testing that pragmas from both files are properly merged - -import pragma_merge_base; - -// Add more pragmas - these should merge with imported ones -pragma no-patterson-condition TestClassC3,TestClassB4; -pragma no-coverage-condition TestClassC3; -pragma no-bounded-variable-condition TestClassB4; - -forall a b . class a:TestClassC3(b) {} -forall a . class a:TestClassB4 {} - -// fails coverage & patterson (pragma set here) -forall i j . (i,j):TestClassP1 => instance i:TestClassC3(j) {} - -// fails coverage & patterson (pragma set in base) -forall i j . (i,j):TestClassP1 => instance i:TestClassP3(j) {} - -// fails bound var & patterson (pragma set here) -forall a c . c:TestClassB1(a) => instance TestType1(a):TestClassB4 {} - -// fails bound var & patterson (pragma set in base) -forall a c . c:TestClassB1(a) => instance TestType1(a):TestClassB3 {} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_verify.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_verify.solc deleted file mode 100644 index 123f9b51..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_merge_verify.solc +++ /dev/null @@ -1,13 +0,0 @@ -// Verification file for pragma merging -// This file imports pragma_merge_base but has no pragmas of its own -// Tests that pragmas from imported files are properly inherited - -import pragma_merge_base; - -data VerifyType(x) = VerifyType; - -// Would fail without imported pragma no-patterson-condition TestClassP3 -forall a . (a,word):TestClassP3(a) => instance a:TestClassP3(word) {} - -// Would fail without imported pragma no-coverage-condition TestClassC1 -forall p q . instance VerifyType(p):TestClassC1(q) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_test_patterson.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_test_patterson.solc deleted file mode 100644 index fd7be0ed..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/pragma_test_patterson.solc +++ /dev/null @@ -1,9 +0,0 @@ -// Simple Patterson test - should fail without pragma - -forall a . class a:C1 {} -forall a . class a:C2 {} - -data T(x) = T; - -// This violates Patterson: context measure (2) >= conclusion measure (2) -forall U . U:C1, U:C2 => instance T(U):C1 {} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy.solc deleted file mode 100644 index a3e30424..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy.solc +++ /dev/null @@ -1,11 +0,0 @@ -data Proxy(a) = Proxy; - -forall self . class self:BaseMemoryType { - function memorySize(x:Proxy(self)) -> word; -} - - -forall t . t : BaseMemoryType => -function morefun(p:Proxy(t)) -> word { - return BaseMemoryType.memorySize(Proxy:Proxy(t)); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy1.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy1.solc deleted file mode 100644 index 34da29be..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/proxy1.solc +++ /dev/null @@ -1,9 +0,0 @@ -data Proxy(a) = Proxy; - -forall a. class a:C { - function fun(p:Proxy(a)) -> word; -} - -forall t. function morefun(p:Proxy(t)) -> word { - return C.fun(Proxy:Proxy(t)); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/rec.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/rec.solc deleted file mode 100644 index 2e663349..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/rec.solc +++ /dev/null @@ -1,6 +0,0 @@ -function rec (n, b, f) { - match n { - | 0 => return b; - | m => return f(primAddWord(m,1), rec(m, b, f)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good.solc deleted file mode 100644 index 15a1c11c..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good.solc +++ /dev/null @@ -1,234 +0,0 @@ - -/////// Construction -forall abs rep . class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x; } -} - -data uint = uint(word); - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -forall a . instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -forall a . instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a . instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self . class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self . class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(MemoryType.load(ptr)); - } - function store(ptr:word, value:uint) -> () { - return MemoryType.store(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) -> () { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field) = MemberAccessProxy(a, field); - -forall a field . -function memberAccessD1(x:MemberAccessProxy(a, field)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -// This is *a lot* of pragmas... -pragma no-coverage-condition StructField, LValueMemberAccess, RValueMemberAccess; -pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; -pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; -forall self fieldType offsetType . class self:CStructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):CStructField(fieldType, offsetType), offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(Typedef.abs(ptr)); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):CStructField(fieldType, offsetType), fieldType:MemoryType, offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - // BUG: Something wrong here? Complains about ptr not being word... - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(Typedef.abs(ptr)):fieldType; - } -} - -////// Testing - -// struct S { x:word; y:uint; z:word; } -data S = S(word, uint, word); -data x_sel = x_sel; -data y_sel = y_sel; -data z_sel = z_sel; - -instance StructField(S, x_sel):CStructField(word, ()) {} -instance StructField(S, y_sel):CStructField(uint, word) {} -// BUG: This next one should really be the following, but that breaks weirdly: -// (I get a patterson condition violation on an invoke instance for g) -// instance StructField(S, z_sel):StructField(word, (word,uint)) {} -// So instead I use: -instance StructField(S, z_sel):CStructField(word, word) {} - - -function f() { - let x:memory(word); - let y:memory(word); - // x = y - Assign.assign(ref(x), y); - /* - * Idea in the above: to avoid overlapping instances, - * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), - * to be able to choose a disjoint assign instance. - * Of course this needs special treatment during code generation, - * on the other hand, stack assignments generally do... - * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. - */ -} - -function g() { - let s:memory(S) = Typedef.abs(0x80); - let y:word = 42; - let z:uint = uint(42); - // s.x = y - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel)), y); - // s.y = 21 - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, y_sel)), z); - // s.z = y; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), y); - // y = s.x - Assign.assign(ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); - // s.z = s.x - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); -} -contract C { - function main() { - f(); - g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good1.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good1.solc deleted file mode 100644 index 14011a02..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding-good1.solc +++ /dev/null @@ -1,235 +0,0 @@ - -/////// Construction -forall abs rep . class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -forall a . instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -forall a . instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a . instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self . class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self . class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(MemoryType.load(ptr)); - } - function store(ptr:word, value:uint) -> () { - return MemoryType.store(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) -> () { - MemoryType.store(Typedef.rep(l), y); - } -} - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x; } -} - - - -data MemberAccessProxy(a, field) = MemberAccessProxy(a, field); - -forall a field . -function memberAccessD1(x:MemberAccessProxy(a, field)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -// This is *a lot* of pragmas... -pragma no-coverage-condition StructField, LValueMemberAccess, RValueMemberAccess; -pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; -pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; -forall self fieldType offsetType . class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):StructField(fieldType, offsetType), offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(Typedef.abs(ptr)); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):StructField(fieldType, offsetType), fieldType:MemoryType, offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - // BUG: Something wrong here? Complains about ptr not being word... - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(Typedef.abs(ptr)):fieldType; - } -} - -////// Testing - -// struct S { x:word; y:uint; z:word; } -data S = S(word, uint, word); -data x_sel = x_sel; -data y_sel = y_sel; -data z_sel = z_sel; - -instance StructField(S, x_sel):StructField(word, ()) {} -instance StructField(S, y_sel):StructField(uint, word) {} -// BUG: This next one should really be the following, but that breaks weirdly: -// (I get a patterson condition violation on an invoke instance for g) -// instance StructField(S, z_sel):StructField(word, (word,uint)) {} -// So instead I use: -instance StructField(S, z_sel):StructField(word, word) {} - - -function f() { - let x:memory(word); - let y:memory(word); - // x = y - Assign.assign(ref(x), y); - /* - * Idea in the above: to avoid overlapping instances, - * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), - * to be able to choose a disjoint assign instance. - * Of course this needs special treatment during code generation, - * on the other hand, stack assignments generally do... - * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. - */ -} - -function g() { - let s:memory(S) = Typedef.abs(0x80); - let y:word = 42; - let z:uint = uint(42); - // s.x = y - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel)), y); - // s.y = 21 - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, y_sel)), z); - // s.z = y; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), y); - // y = s.x - Assign.assign(ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); - // s.z = s.x - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); -} -contract C { - function main() { - f(); - g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding.solc deleted file mode 100644 index 37fde308..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-encoding.solc +++ /dev/null @@ -1,230 +0,0 @@ - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(MemoryType.load(ptr)); - } - function store(ptr:word, value:uint) -> () { - return MemoryType.store(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field) = MemberAccessProxy(a, field); - -forall a field . -function memberAccessD1(x:MemberAccessProxy(a, field)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -// This is *a lot* of pragmas... -pragma no-coverage-condition StructField, LValueMemberAccess, RValueMemberAccess; -pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; -pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):StructField(fieldType, offsetType), offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(Typedef.abs(ptr)); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a b. a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType . StructField(structType, fieldSelector):StructField(fieldType, offsetType), fieldType:MemoryType, offsetType:MemorySize => instance MemberAccessProxy(memory(structType), fieldSelector):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - // BUG: Something wrong here? Complains about ptr not being word... - /*assembly { - ptr := add(ptr, size) - }*/ - return MemoryType.load(Typedef.abs(ptr)):fieldType; - } -} - -////// Testing - -// struct S { x:word; y:uint; z:word; } -data S = S(word, uint, word); -data x_sel = x_sel; -data y_sel = y_sel; -data z_sel = z_sel; - -instance StructField(S, x_sel):StructField(word, ()) {} -instance StructField(S, y_sel):StructField(uint, word) {} -// BUG: This next one should really be the following, but that breaks weirdly: -// (I get a patterson condition violation on an invoke instance for g) -// instance StructField(S, z_sel):StructField(word, (word,uint)) {} -// So instead I use: -instance StructField(S, z_sel):StructField(word, word) {} - - -function f() { - let x:memory(word); - let y:memory(word); - // x = y - Assign.assign(ref(x), y); - /* - * Idea in the above: to avoid overlapping instances, - * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), - * to be able to choose a disjoint assign instance. - * Of course this needs special treatment during code generation, - * on the other hand, stack assignments generally do... - * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. - */ -} - -function g() { - let s:memory(S) = Typedef.abs(0x80); - let y:word = 42; - let z:uint = uint(42); - // s.x = y - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel)), y); - // s.y = 21 - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, y_sel)), z); - // s.z = y; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), y); - // y = s.x - Assign.assign(ref(y), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); - // s.z = s.x - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, z_sel)), RValueMemberAccess.memberAccess(MemberAccessProxy(s, x_sel))); -} -contract C { - function main() { - f(); - g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-test.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-test.solc deleted file mode 100644 index 6bb282d7..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference-test.solc +++ /dev/null @@ -1,56 +0,0 @@ -data memory(a) = memory(word); - -class abs:Typedef(rep) { - function abs(v:rep) -> abs; - function rep(v:abs) -> rep; -} - -instance memory(a):Typedef(word) { - function abs(ptr:word) -> memory(a) { - return memory(ptr); - } - function rep(v:memory(a)) -> word { - match v { - | memory(ptr) => return ptr; - } - } -} - -pragma no-patterson-condition Test; -pragma no-bounded-variable-condition Test; -class self:Test { - function test(x:self) -> word; -} - -instance word:Test { - function test(x:word) -> word { - return x; - } -} - -data test(a) = test(memory(a)); - -instance test(a):Typedef(memory(a)) { - function rep(x:test(a)) -> memory(a) { - match x { - | test(m) => return m; - } - } - function abs(m:memory(a)) -> test(a) { - return test(m); - } -} - -forall abs rep . test(abs):Typedef(rep), rep:Test => - instance test(abs):Test { - function test(x:test(abs)) -> word { - return Test.test(Typedef.rep(x)); - } - } - -contract C { - function main() { - let x:test(word) = test(memory(42)); - let ptr:word = Test.test(x); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference.solc deleted file mode 100644 index dfbb9394..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/reference.solc +++ /dev/null @@ -1,32 +0,0 @@ -class ref : Ref(deref) { - function load (r:ref) -> deref; - function store(r:ref, v:deref) -> unit; -} - -data stack(a) = stack(a); - -instance stack(a) : Ref(a) { -} - -data MemberAccess(ty, field) = MemberAccess(ty); - -data PairFst = PairFst; -data PairSnd = PairSnd; - -pragma no-bounded-variable-condition Ref; -data XRef(st, field, fieldType) = XRef(st, field); -forall r : Ref (a,b) . instance XRef(r, PairFst, a) : Ref(a) {} -forall r : Ref (a,b) . instance XRef(r, PairSnd, b) : Ref(b) {} - -contract AssignNested { - function main() { - let x : stack( (word, (word, word)) ); - let z : stack( (word, (word, word)) ); - - // either of the next lines is fine on their own, but not together - Ref.store( XRef(z,PairFst), 21); - Ref.store( XRef(XRef(x, PairSnd), PairFst), 20 ); - - return 77; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/references-daniel.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/references-daniel.solc deleted file mode 100644 index 700ef751..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/references-daniel.solc +++ /dev/null @@ -1,239 +0,0 @@ -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - -data xunit = xunit; - -data uint = uint(word); - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(MemoryType.load(ptr)); - } - function store(ptr:word, value:uint) -> () { - return MemoryType.store(ptr, Typedef.rep(value)); - } -} - -forall a . a:MemoryType => -instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field) = MemberAccessProxy(a, Proxy(field)); - -forall a field . -function memberAccessPtr(x:MemberAccessProxy(memory(a), field)) -> word { - match x { - | MemberAccessProxy(y,z) => match y { - | memory(ptr) => return ptr; - } - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -instance xunit:MemorySize { - function size(x:Proxy(xunit)) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -data zero = zero; -data suc(a) = suc(a); - -forall a b . instance MemberAccessProxy(memory((a, b)), zero) : Typedef (word) {} -forall a b . instance MemberAccessProxy(memory((a,b)), zero):LValueMemberAccess(memoryRef(a)) { - function memberAccess(mptr:MemberAccessProxy(memory((a,b)), zero), f:Proxy(zero)) -> memoryRef(a) { - let ptr:word = Typedef.rep(mptr); - return memoryRef(ptr); - } -} -pragma no-coverage-condition LValueMemberAccess; -pragma no-patterson-condition LValueMemberAccess; -pragma no-bounded-variable-condition LValueMemberAccess; - -forall a b c n. MemberAccessProxy(memory(b), n):LValueMemberAccess(c), a:MemorySize => -instance MemberAccessProxy(memory((a,b)), suc(n)):LValueMemberAccess(c) { - function memberAccess(map:MemberAccessProxy(memory((a,b)), suc(n)), f:Proxy(suc(n))) -> c { - let ptr:word = memberAccessPtr(map); - let sz:word = MemorySize.size(Proxy:Proxy(a)); - assembly { ptr := add(ptr, sz) } - let newPtr:memory(b) = memory(ptr); - return LValueMemberAccess.memberAccess(MemberAccessProxy(newPtr, Proxy:Proxy(n))); - } -} - -instance MemberAccessProxy(memory(a), zero) : LValueMemberAccess (word) {} -instance MemberAccessProxy(memory(a), suc(zero)) : LValueMemberAccess (uint) {} -instance MemberAccessProxy(memory(a), suc(suc(zero))) : LValueMemberAccess (word) {} -instance word:Assign(word){} -instance uint:Assign(uint){} - -////// Testing - -// struct S { x:word; y:uint; z:word; } -data S = S(word, uint, word); -data x_sel = x_sel; -data y_sel = y_sel; -data z_sel = z_sel; - -instance S:Typedef((word, uint, word)) { - function abs(x:(word, uint, word)) -> S { - match x { - | (a, b, c) => return S(a, b, c); - } - } - function rep(x:S) -> (word, uint, word) { - match x { - | S(a, b, c) => return (a, b, c); - } - } -} - - -// The idea here would be to generate these particularly on the definition of a struct with fields. -forall c rep . S:Typedef(rep), MemberAccessProxy(memory(rep), zero):LValueMemberAccess(word) => -instance MemberAccessProxy(memory(S), x_sel):LValueMemberAccess(word) { - function memberAccess(map:MemberAccessProxy(memory(S), x_sel), f:Proxy(x_sel)) -> word { - return (LValueMemberAccess.memberAccess(MemberAccessProxy(memory(memberAccessPtr(map)):memory(rep), Proxy:Proxy(zero))) : word); - } -} - -forall c rep . S:Typedef(rep), MemberAccessProxy(memory(rep), suc(zero)):LValueMemberAccess(uint) => -instance MemberAccessProxy(memory(S), y_sel):LValueMemberAccess(uint) { - function memberAccess(map:MemberAccessProxy(memory(S), y_sel), f:Proxy(y_sel)) -> uint { - return LValueMemberAccess.memberAccess(MemberAccessProxy(memory(memberAccessPtr(map)):memory(rep), Proxy:Proxy(suc(zero)))); - } -} - -forall c rep . S:Typedef(rep), MemberAccessProxy(memory(rep), suc(suc(zero))):LValueMemberAccess(word) => -instance MemberAccessProxy(memory(S), z_sel):LValueMemberAccess(word) { - function memberAccess(map:MemberAccessProxy(memory(S), z_sel), f:Proxy(z_sel)) -> word { - return LValueMemberAccess.memberAccess(MemberAccessProxy(memory(memberAccessPtr(map)):memory(rep), Proxy:Proxy(suc(suc(zero))))); - } -} - -function f() { - let x:memory(word); - let y:memory(word); - x = y; -} - -function g() { - let s:memory(S) = Typedef.abs(0x80); - let x:word = 42; - let y:uint = Typedef.abs(21); - let z:word = 7; - // s.x = x; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, Proxy:Proxy(x_sel))), x); - // s.y = y; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, Proxy:Proxy(y_sel))), y); - // s.z = z; - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(s, Proxy:Proxy(z_sel))), z); -} - -contract C { - function main() { - f(); - g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/signature.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/signature.solc deleted file mode 100644 index 1be7243b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/signature.solc +++ /dev/null @@ -1,8 +0,0 @@ -class self:Typedef(underlyingType) { - function rep(x:self) -> underlyingType; -} - - -forall t:Typedef(word) . function tripleFun(x:t) { - return Typedef.rep(x); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfExpr.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfExpr.solc deleted file mode 100644 index 53b0d57e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfExpr.solc +++ /dev/null @@ -1,3 +0,0 @@ -contract SimpleIfStmt { - function main() { return (if (true) then 1 else 0); } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfStmt.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfStmt.solc deleted file mode 100644 index ab9d49dd..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleIfStmt.solc +++ /dev/null @@ -1,3 +0,0 @@ -contract SimpleIfStmt { - function main() { if (true) {return 1;} else {return 0;} } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleid.solc deleted file mode 100644 index 63de8ede..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/simpleid.solc +++ /dev/null @@ -1,3 +0,0 @@ -function id(x) { - return x; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/single-lambda.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/single-lambda.solc deleted file mode 100644 index 7c6a1729..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/single-lambda.solc +++ /dev/null @@ -1,3 +0,0 @@ -function foo () -> (word) -> bool { - return lam (x:word) -> bool { return true; }; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/string-const.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/string-const.solc deleted file mode 100644 index 204fb34e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/string-const.solc +++ /dev/null @@ -1,5 +0,0 @@ -contract Answer { - function main() { - return "42"; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-index.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-index.solc deleted file mode 100644 index 969f937b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-index.solc +++ /dev/null @@ -1,77 +0,0 @@ -data storage(a) = storage(word); -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -forall a . instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) { - } -} - -forall self fieldType offsetType . class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(cxt, fieldSelector):StructField(fieldType, offsetType) - => instance MemberAccessProxy(cxt, fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(cxt, fieldSelector, offsetType)) -> storageRef(fieldType) { - return storageRef(0x100); - } -} - -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); - -forall map index member. - instance IndexAccessProxy(storageRef(map), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(map), index, member)) -> storageRef(member) { - return storageRef(0); - } -} - -data MintCtx = MintCtx; -data balances_sel = balances_sel; -instance StructField(MintCtx, balances_sel):StructField(mapping(word,word), ()) {} - - function mint(amount:word) { - let bal_prx = MemberAccessProxy(MintCtx, balances_sel); - let bal_ref = LValueMemberAccess.memberAccess(bal_prx); - - Assign.assign( - LValueMemberAccess.memberAccess( - IndexAccessProxy( - // bal_ref // this works, but inlining bal_ref leads to error - LValueMemberAccess.memberAccess(bal_prx) - , 0 - ) - ) - , amount - ) ; - - } -contract Map { - function main () { - mint(1000); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-reduction.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-reduction.solc deleted file mode 100644 index 71c44901..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subject-reduction.solc +++ /dev/null @@ -1,81 +0,0 @@ -data storage(a) = storage(word); -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); - -forall lhs rhs . class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -forall a . instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) { - } -} - -forall self fieldType offsetType . class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - - -forall self memberRefType . class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(cxt, fieldSelector):StructField(fieldType, offsetType) - => instance MemberAccessProxy(cxt, fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(cxt, fieldSelector, offsetType)) -> storageRef(fieldType) { - return storageRef(0x100); - } -} - -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); - -forall map index member. - instance IndexAccessProxy(storageRef(map), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(map), index, member)) -> storageRef(member) { - return storageRef(0); - } -} - -pragma no-coverage-condition LValueMemberAccess; - -data MintCtx = MintCtx; -data balances_sel = balances_sel; -instance StructField(MintCtx, balances_sel):StructField(mapping(word,word), ()) {} - - function mint(amount:word) { - let bal_prx = MemberAccessProxy(MintCtx, balances_sel); - let bal_ref = LValueMemberAccess.memberAccess(bal_prx); - - Assign.assign( - LValueMemberAccess.memberAccess( - IndexAccessProxy( - // bal_ref // this works, but inlining bal_ref leads to error - LValueMemberAccess.memberAccess(bal_prx) - , 0 - ) - ) - , amount - ) ; - - } -/* -contract Map { - function main () { - mint(1000); - } -} -*/ diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-constraint.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-constraint.solc deleted file mode 100644 index 339a84e3..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-constraint.solc +++ /dev/null @@ -1,22 +0,0 @@ -// This code should FAIL, but PASSES! -data Bool = True | False; - -forall a . class a : MyCls { - function f(x : a, y : a) -> Bool; -} - -forall a . function the_bug(x : a, y : a) -> Bool { - return MyCls.f(x, y); -} - -contract Foo { - function x() { - let b1 = True; - let b2 = False; - the_bug(b1, b2); - } - - function main() { - x(); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-test.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-test.solc deleted file mode 100644 index 014b0a36..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/subsumption-test.solc +++ /dev/null @@ -1,7 +0,0 @@ -function id (x) -> word { - return x; -} - -forall a . function fakeid(x : word) -> a { - return x ; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class-num.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class-num.solc deleted file mode 100644 index 9ae763fc..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class-num.solc +++ /dev/null @@ -1,70 +0,0 @@ -data Bool = False | True; - -function fromBool(b:Bool) -> word { - match b { - | False => return 0; - | True => return 1; - } -} - -function toBool(x: word) -> Bool { - match x { - | 0 => return False; - | _ => return True; - } -} - -forall a. -class a:Eq { - function eq(x:a, y:a) -> Bool; -} - -instance word:Eq { - function eq(x:word, y:word) -> Bool { - let res : word; - assembly { - res := eq(x, y) - } - return toBool(res); - } -} - -function not (b) { - match b { - | True => return False ; - | False => return True ; - } -} - -function ne(x, y) { - return not(Eq.eq(x,y)); -} - -forall a. a:Eq => -class a:Num { - function toWord(x:a) -> word; - function fromWord(x:word) -> a; -} - -instance word:Num { - function toWord(x:word) -> word { return x; } - function fromWord(x:word) -> word { return x; } -} - - -data uint = uint(word); - -instance uint:Eq { - function eq(x:uint, y:uint) -> Bool { return Eq.eq(Num.toWord(x), Num.toWord(y)); } -} - - -instance uint:Num { - function toWord(x:uint) -> word - { - match x { - | uint(y) => return y; - } - } - function fromWord(x:word) -> uint { return uint(x); } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class.solc deleted file mode 100644 index dd85b793..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/super-class.solc +++ /dev/null @@ -1,37 +0,0 @@ -data List(a) = Nil | Cons(a,List(a)); -data Bool = False | True; - -function and (x,y) { - match x,y { - | False, _ => return False; - | True, y => return y; - } -} - -forall a . class a : Eq { - function eq(x : a, y : a) -> Bool; -} - -instance Bool : Eq { - function eq (x : Bool, y : Bool) -> Bool { - match x, y { - | False, False => return True; - | True, True => return True; - | _, _ => return False; - } - } -} - -forall a . a : Eq => instance (List(a)) : Eq { - function eq (xs : List(a), ys : List(a)) -> Bool { - match xs, ys { - | Nil, Nil => return True; - | Cons(x,xs), Cons(y,ys) => - return and(Eq.eq(x,y),Eq.eq(xs,ys)); - } - } -} - -function foo() { - let x = Eq.eq(Cons(True,Nil), Nil); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-basic.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-basic.solc deleted file mode 100644 index 2f521980..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-basic.solc +++ /dev/null @@ -1,21 +0,0 @@ -type Uint = word; -type Point = pair(word, word); - -function useUint(x: Uint) -> word { - return x; -} - -function makePoint(x: word, y: word) -> Point { - return pair(x, y); -} - -function getX(p: Point) -> word { - match p { - | pair(x, _) => return x; - } -} - -function main() -> word { - let p: Point = makePoint(10, 20); - return getX(p); -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-in-function.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-in-function.solc deleted file mode 100644 index a71676b0..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-in-function.solc +++ /dev/null @@ -1,30 +0,0 @@ -// Synonyms in function parameter and return types -type Int = word; -type Point = pair(Int, Int); - -function add(a: Int, b: Int) -> Int { - return a; -} - -function makePoint(x: Int, y: Int) -> Point { - return pair(x, y); -} - -function getX(p: Point) -> Int { - match p { - | pair(x, _) => return x; - } -} - -function getY(p: Point) -> Int { - match p { - | pair(_, y) => return y; - } -} - -function main() -> word { - let a: Int = 10; - let b: Int = 20; - let p: Point = makePoint(a, b); - return getX(p); -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-long-cycle.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-long-cycle.solc deleted file mode 100644 index d06783dc..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-long-cycle.solc +++ /dev/null @@ -1,8 +0,0 @@ -// Longer recursive cycle should be rejected -type A = B; -type B = C; -type C = A; - -function main() -> word { - return 0; -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-nested.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-nested.solc deleted file mode 100644 index 912cc705..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-nested.solc +++ /dev/null @@ -1,24 +0,0 @@ -// Deeply nested synonyms (synonym of synonym of synonym) -type Word1 = word; -type Word2 = Word1; -type Word3 = Word2; - -type Pair1 = pair(word, word); -type Pair2 = Pair1; -type Pair3 = Pair2; - -function useWord3(x: Word3) -> word { - return x; -} - -function usePair3(p: Pair3) -> word { - match p { - | pair(x, _) => return x; - } -} - -function main() -> word { - let x: Word3 = 42; - let p: Pair3 = pair(1, 2); - return useWord3(x); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-param.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-param.solc deleted file mode 100644 index 1ed3f566..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-param.solc +++ /dev/null @@ -1,13 +0,0 @@ -type MyPair(a, b) = pair(a, b); -type IntPair = MyPair(word, word); - -function makePair(x: word, y: word) -> MyPair(word, word) { - return pair(x, y); -} - -function main() -> word { - let p: IntPair = makePair(42, 100); - match p { - | pair(x, _) => return x; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-recursive.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-recursive.solc deleted file mode 100644 index ae490f92..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-recursive.solc +++ /dev/null @@ -1,8 +0,0 @@ -type A = B; -type B = A; - -contract RecursiveTest { - function main() -> word { - return 0; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-self-recursive.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-self-recursive.solc deleted file mode 100644 index 9ecb567d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/synonym-self-recursive.solc +++ /dev/null @@ -1,6 +0,0 @@ -// Self-recursive synonym should be rejected -type A = A; - -function main() -> word { - return 0; -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tiamat.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/tiamat.solc deleted file mode 100644 index 31747f88..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tiamat.solc +++ /dev/null @@ -1,136 +0,0 @@ -data Proxy (a) = Proxy ; -data dict(member, index) = dict(word, Proxy(member), Proxy(index)) ; -data address = address(word) ; -data storage(a) = storage(word) ; - -forall a. -function saddr(s: storage(a)) -> word { - match s { - | storage(a) => return a; - } -} - - -// Untyped Index (access) Proxy -data UIP (m, idx, member) = UIP(m ,idx); -// Typed Index (access) Proxy -data TIP (m, idx, member) = TIP(m ,idx, Proxy(member)); - -function setbal(ref: storage(dict(address, word)) , src : address, amt: word) { - /* Based on inference: - ref : storage(dict(address, word)) - => ref[src] : storage(word) assuming src is of the right type - */ - let tip = TIP(ref, src, Proxy:Proxy(word)); - Assign.assign(LVA.acc(tip), amt); -} - -function setAllowance(ref: storage(dict(address, dict(address, word))), owner : address, spender : address, amt : word) -> () { - - let tip1 : TIP(storage(dict(address, dict(address, word))), address, dict(address, word)) - = TIP(ref, owner, Proxy:Proxy(dict(address, word) )); - let ref2 : storage(dict(address,word)) = LVA.acc(tip1); - let tip2 : TIP(storage(dict(address, word)), address, word) - = TIP(ref2, spender, Proxy:Proxy(word)); - let ref3 : storage(word) = LVA.acc(tip2); - Assign.assign(ref3, amt); -} - -function getAllowance(ref: storage(dict(address, dict(address, word))), owner : address, spender : address) -> word { -/* - let tip : TIP(storage(dict(address, dict(address, word))), address, dict(address, word)) - = TIP(ref, owner, Proxy:Proxy(dict(address, word) )); - let ref2 : storage(dict(address,word)) = LVA.acc(tip); - let tip2 : TIP(storage(dict(address, word)), address, word) - = TIP(ref2, spender, Proxy:Proxy(word)); -*/ - return RVA.acc( - TIP - ( LVA.acc( - TIP - (ref - , owner - , Proxy:Proxy(dict(address, word) ) - ) /* tip : TIP(storage(dict(address, dict(address, word))), address, dict(address, word)) */ - ) /* ref2 : storage(dict(address,word)) */ - , spender - , Proxy:Proxy(word) - ) /* tip2 : TIP(storage(dict(address, word)), address, word) */ - ); -} - -forall self memberRefType. -class self:LVA(memberRefType) { - function acc(x:self) -> memberRefType; -} - - -forall self member. -class self:RVA(member) { - function acc(x:self) -> member; -} - -forall index member. - instance TIP(storage(dict(index,member)), index, member):LVA(storage(member)) { - function acc(x:TIP(storage(dict(index,member)), index, member)) -> storage(member) { - return storage(42); - } -} - -forall index member. - instance UIP(storage(dict(index,member)), index, member):LVA(storage(member)) { - function acc(x:UIP(storage(dict(index,member)), index, member)) -> storage(member) { - return storage(42); - } -} - -forall self. -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -forall index member. member:StorageType => - instance TIP(storage(dict(index,member)), index, member):RVA(member) { - function acc(x:TIP(storage(dict(index,member)), index, member)) -> member { - let addr = saddr(LVA.acc(x)); - return StorageType.sload(addr); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - - -forall a. a:StorageType => -instance storage(a):Assign(a) { - function assign(l:storage(a), r:a) -> () { - StorageType.store(saddr(l), r); - } -} - -contract Tiamat { - function main() { - let allowances : storage(dict(address, dict(address, word))); - let src = address(17); - setAllowance(allowances, address(1),address(2), 666); - return getAllowance(allowances, address(1),address(2)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuple-trick.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuple-trick.solc deleted file mode 100644 index 09367079..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuple-trick.solc +++ /dev/null @@ -1,41 +0,0 @@ -pragma no-coverage-condition Nth; - - -data Zero; -data Succ(a); - -data Proxy(a) = Proxy; - -forall a b c . class a : Nth(b,c) { - function nth (x : Proxy(a), y : b) -> c; -} - -forall a b . instance Zero : Nth((a,b), a) { - function nth (x : Proxy(Zero), y : (a,b)) -> a { - match y { - | (a, b) => return a ; - } - } -} - -forall n a b c . n : Nth (b,c) => instance Succ(n) : Nth ((a,b), c) { - function nth (x : Proxy(Succ(n)), y : (a,b)) -> c { - match y { - | (a,b) => return Nth.nth(Proxy : Proxy(n), b); - } - } -} - -contract C { - function id (x) { - return x; - } - function main () { - let p : (word, word, word, ()); - let x : word = Nth.nth(Proxy : Proxy(Zero), p); - let y : word = Nth.nth(Proxy : Proxy(Succ(Zero)), p); - let z : word = Nth.nth(Proxy : Proxy(Succ(Succ(Zero))), p); - id(z); - } -} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuva.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuva.solc deleted file mode 100644 index c47481e3..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tuva.solc +++ /dev/null @@ -1,77 +0,0 @@ -// TUVA: TUple-based Value Access -/* -# Types and classes for assignemnt desugaring using -- access proxy types -- LValue and RValue access classes (LVA, RVA) -- StorageType class -- Assign class -*/ - -import std; - - -forall col_idx val . class col_idx:RValueIdxAccess(val) { - function lookup(ci : col_idx) -> val; -} - -forall col_idx ref . class col_idx:LValueIdxAccess(ref) { - function lookup(ci : col_idx) -> ref; -} - -forall i a . i:Typedef(word) => -instance (storage(mapping(i,a)), i): LValueIdxAccess(storage(a)) { - function lookup(xi : (storage(mapping(i,a)), i)) -> storage(a) { - match(xi) { - | (x, i) => return storage(hash2(Typedef.rep(x), Typedef.rep(i))); - } - - // return storage(42); // FIXME: hash2(x,i); - } -} - -forall i a . a:StorageType, i:Typedef(word) => -instance (storage(mapping(i,a)), i): RValueIdxAccess(a) { - function lookup(xi : (storage(mapping(i,a)), i)) -> a { - /* - match(xi) { - | (x, i) => return StorageType.sload(hash2(Typedef.rep(x), Typedef.rep(i))); - } - */ - return readStorage(LValueIdxAccess.lookup(xi)); - } -} - -forall a. a:StorageType => -function readStorage(x:storage(a)) -> a { - return StorageType.sload(Typedef.rep(x)); -} - -forall r a. r: RValueIdxAccess(a) => -function idx_rval(x:r) -> a { - return RValueIdxAccess.lookup(x); -} - -forall r a. r: LValueIdxAccess(a) => -function idx_lval(x:r) -> a { - return LValueIdxAccess.lookup(x); -} - -contract TestTuva { - function main() -> word { - let balances : storage(mapping(address, word)); - let allowances : storage(mapping(address, mapping(address, word) )); - let ref1 : storage(word) = idx_lval( (balances, address(17)) ); - Assign.assign(idx_lval( (balances, address(1)) ), 1337); - - let ref2a // : storage( mapping(address, word) ) // omitting this type makes instance resolution fail - = idx_lval ( (allowances, address(1)) ); - - let ref2b // : storage( word ) - = idx_lval ( (ref2a, address(2)) ); - - Assign.assign( ref2b, 777 ); - -// return idx_rval( (balances, address(1)) ); - return idx_rval ( (ref2a, address(2)) ); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tyexp.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/tyexp.solc deleted file mode 100644 index 1a1f3f80..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/tyexp.solc +++ /dev/null @@ -1,4 +0,0 @@ -function main () { - let y = 0 : word ; - return y; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/typedef.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/typedef.solc deleted file mode 100644 index 1421e691..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/typedef.solc +++ /dev/null @@ -1,9 +0,0 @@ -forall self underlyingType . class self:Typedef(underlyingType) { - function rep(x:self) -> underlyingType; - function abs(x:underlyingType) -> self; -} - -forall t . t : Typedef((word,(word,word))) => - function tripleFun(x:t) -> (word, (word, word)) { - return Typedef.rep(x); - } diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/uintdesugared.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/uintdesugared.solc deleted file mode 100644 index 0439ab6a..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/uintdesugared.solc +++ /dev/null @@ -1,512 +0,0 @@ -// Before desugaring: -/* -import IndexLib; - -contract Uint { - reserved : word; - owner : address; - decimals : uint; - totalSupply : uint; - balances : mapping(address,uint); - - function mint(amount:uint) { - balances[owner] = Num.add(balances[owner], amount); - totalSupply = Num.add(totalSupply, amount); - } - - function init() { - owner = address(0x123456789abcdef); - decimals = Num.fromWord(18); - } - function main() -> uint { - init(); - mint(uint(1000)); - mint(uint(1000)); - return balances[owner] : uint; - } -} -*/ - - -function addW(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -function subW(x : word, y : word) { - let res: word; - assembly { - res := sub(x, y) - } - return res; -} - -function addU(x : uint, y : uint) -> uint { - let res: word; - let xw : word = Num.toWord(x); - let yw : word = Num.toWord(y); - assembly { - res := add(xw, yw) - } - return uint(res); -} - -function hash1(x: word) -> word { - let result: word = 0; - assembly { - mstore(0, x) - result := keccak256(0,32) - } - return result; -} - -function hash2(x: word, y: word) -> word { - let result: word = 0; - assembly { - mstore(0, x) - mstore(32, y) - result := keccak256(0,64) - } - return result; -} - -forall a. -class a:Num { - function toWord(x:a) -> word; - function fromWord(x:word) -> a; - function add(x:a, y:a) -> a; - function sub(x:a, y:a) -> a; -} - -instance word:Num { - function toWord(x:word) -> word { return x; } - function fromWord(x:word) -> word { return x; } - function add(x:word, y:word) -> word { return addW(x, y); } - function sub(x:word, y:word) -> word { return addW(x, y); } -} - -data uint = uint(word); - -instance uint:Num { - function toWord(x:uint) -> word - { - match x { - | uint(y) => return y; - } - } - - function fromWord(x:word) -> uint { return uint(x); } - function add(x:uint, y:uint) -> uint { return uint(addW(Num.toWord(x), Num.toWord(y))); } - function sub(x:uint, y:uint) -> uint { return uint(subW(Num.toWord(x), Num.toWord(y))); } -} - -/* // this breaks the Paterson condition -forall a. a:Typedef(word) => -instance a:Num { - function toWord(x:a) -> word { return Typedef.rep(x); } - function fromWord(x:word) { return Typedef.abs(x); } - function add(x:a, y:a) -> a { return Typedef.abs(addW(Typedef.rep(x), Typedef.rep(y))); } -} -*/ - -// Storage slots and mapping access - - -/////// Construction -forall abs rep. -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x; } -} - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data address = address(word); - -instance address:Typedef(word) { - function rep(x:address) -> word { - match x { - | address(y) => return y; - } - } - function abs(x:word) -> address { - return address(x); - } -} - -data storage(a) = storage(word); -data ContractStorage(cxt) = ContractStorage(cxt); - -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -data mapRef(a) = mapRef(word); //ref to a map elem - -// data memoryRef(a) = memoryRef(word); - -forall a. -instance storage(a):Typedef(word) { - function rep(x:storage(a)) -> word { - match x { - | storage(y) => return y; - } - } - function abs(x:word) -> storage(a) { - return storage(x); - } -} - -forall a. -instance storageRef(a):Typedef(word) { - function rep(x:storageRef(a)) -> word { - match x { - | storageRef(y) => return y; - } - } - function abs(x:word) -> storageRef(a) { - return storageRef(x); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a. -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self. -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self. -class self:StorageSize { - function size(x:Proxy(self)) -> word; -} - - -function sload_(x:word) -> word { - let res: word; - assembly { - res := sload(x) - } - return res; - } - -function sstore_(a:word, v:word) { - assembly { sstore(a,v) } -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -instance uint:StorageType { - function sload(ptr:word) -> uint { - return Typedef.abs(sload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -instance address:StorageType { - function sload(ptr:word) -> address { - return Typedef.abs(sload_(ptr)):address; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:address) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : StorageType => instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) -> () { - StorageType.store(Typedef.rep(l), y); - } -} - -forall self fieldType offsetType. -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType. -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return storageRef(ptr); - } -} - -instance ():StorageSize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:StorageSize { - function size(x:Proxy(word)) -> word { - return 1; - } -} - -instance uint:StorageSize { - function size(x:Proxy(uint)) -> word { - return 1; - } -} - -instance address:StorageSize { - function size(x:Proxy(address)) -> word { - return 1; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:StorageSize -=> instance a:StorageSize { - function size(x:Proxy(a)) -> word { - return StorageSize.size(Proxy(b)); - } -} -*/ - -forall a b . a:StorageSize, b:StorageSize => instance (a,b):StorageSize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = StorageSize.size(Proxy:Proxy(a)); - let b_sz:word = StorageSize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -pragma no-patterson-condition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances -pragma no-coverage-condition MemberAccessProxy, LValueMemberAccess, RValueMemberAccess; - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = 0x100; // forge uses at least 1 storage slot - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - - assembly { - ptr := add(ptr, offsetSize) - } - return storageRef(ptr); // contract storage starts at 0 - } -} - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} - -/* -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} -*/ -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); - -forall member index . instance mapping(index, member):Typedef(word) { - function rep(x:mapping(index, member)) -> word { - match x { - | mapping(y) => return y; - } - } - function abs(x:word) -> mapping(index,member) { - return mapping(x); - } -} - - -// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays -forall index member . -instance mapping(index, member):StorageSize { - function size(x:Proxy(mapping(index, member))) -> word { - return 1; - } -} - -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); - -forall map index member. index:Typedef(word), map:Typedef(word) -=> instance IndexAccessProxy(map, index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(map, index, member)) -> storageRef(member) { - return storageRef(indexStorageSlot(x)); - } -} - -forall map index member . index:Typedef(word), member:StorageType, map:Typedef(word) -=> instance IndexAccessProxy(map, index, member):RValueMemberAccess(member) { - function memberAccess(x:IndexAccessProxy(map, index, member)) -> member { - let slot:word = indexStorageSlot(x); - return StorageType.sload(slot); - } -} - -forall index map member. map:Typedef(word), index:Typedef(word) => function indexStorageSlot(x:IndexAccessProxy(map, index, member)) -> word -//function indexStorageSlot(x) -{ - match x { - | IndexAccessProxy(map, i) => - let mapptr:word = Typedef.rep(map); - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} - -/* -forall index map member. map:Typedef(word), index:Typedef(word) -=> function indexedSlot(mapref : storageRef(mapping(index, member)), i: index) -> word -{ - match mapref { - | storageRef(mapptr) => - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} -*/ - -forall a b. a:RValueMemberAccess(b) => -function rval(x:a) -> b { - return RValueMemberAccess.memberAccess(x); -} - -data UintCxt = UintCxt ; -data reserved_sel = reserved_sel ; -instance StructField(ContractStorage(UintCxt), reserved_sel) : StructField (word, ()) { -} -data owner_sel = owner_sel ; -instance StructField(ContractStorage(UintCxt), owner_sel) : StructField (address, (word, ())) { -} -data decimals_sel = decimals_sel ; -instance StructField(ContractStorage(UintCxt), decimals_sel) : StructField (uint, (word, (address, ()))) { -} -data totalSupply_sel = totalSupply_sel ; -instance StructField(ContractStorage(UintCxt), totalSupply_sel) : StructField (uint, (word, (address, (uint, ())))) { -} -data balances_sel = balances_sel ; -instance StructField(ContractStorage(UintCxt), balances_sel) : StructField (mapping(address, uint), (word, (address, (uint, (uint, ()))))) { -} -contract Uint { - function mint (amount : uint) { - Assign.assign(LValueMemberAccess.memberAccess(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)))), Num.add(rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)))), amount)); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), totalSupply_sel)), Num.add(rval(MemberAccessProxy(ContractStorage(UintCxt), totalSupply_sel)), amount)); - } - function init () { - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)), address(81985529216486895)); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), decimals_sel)), Num.fromWord(18)); - } - function main () -> uint { - init(); - mint(uint(1000)); - mint(uint(1000)); - return rval(IndexAccessProxy(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(UintCxt), balances_sel)), rval(MemberAccessProxy(ContractStorage(UintCxt), owner_sel)))) : uint; - } -} - diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unbound-instance-var.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/unbound-instance-var.solc deleted file mode 100644 index 7b2e1a8b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unbound-instance-var.solc +++ /dev/null @@ -1,16 +0,0 @@ -forall self. -class self:C { - function size(x:self) -> word; -} - -instance ():C { - function size(x:()) -> word { - return 0; - } -} - -instance uint:C { - function size(x:uint) -> word { - return 1; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unconstrained-instance.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/unconstrained-instance.solc deleted file mode 100644 index ecba6e1e..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unconstrained-instance.solc +++ /dev/null @@ -1,24 +0,0 @@ -data memory(t) = memory(word); - -class t:ValueTy { - function rep(x:t) -> word; -} - -instance memory(t) : ValueTy { - function rep(x: memory(t)) -> word { - match x { - | memory(w) => return w; - } - } -} - -class ref:Ref(deref) { - function store(loc: ref, value: deref) -> (); -} - -instance memory(t) : Ref(t) { - function store(loc: memory(t), value: t) -> () { - // We don't have a `ValueTy` bound on `t` anywhere, so this should raise a type error... - let vw = ValueTy.rep(value); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/undefined.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/undefined.solc deleted file mode 100644 index 00943b81..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/undefined.solc +++ /dev/null @@ -1,13 +0,0 @@ -forall any.function undefined() -> any { - assembly { - revert(0,0) - } -} - -function useWord(w:word) {} - -contract Magic { - function main() { - useWord(undefined()); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unit.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/unit.solc deleted file mode 100644 index a697c1b8..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/unit.solc +++ /dev/null @@ -1,33 +0,0 @@ -contract Unit { -function one (x : ()) { - return 1; -} - -function unitVal() { - return (); -} - -function unitMatch (x) { - match x { - | () => return 1; - } -} - -function foo (x : word) { - return (); -} - -function main() { - return unitMatch(foo(one(unitVal()))); -} -} - -forall a . class a : Def { - function def () -> a ; -} - -instance () : Def { - function def() -> () { - return (); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/vartyped.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/vartyped.solc deleted file mode 100644 index 3b89a402..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/vartyped.solc +++ /dev/null @@ -1,4 +0,0 @@ -function foo () { - let f : (word) -> word = lam (x) { return x ; } ; - return f(1); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/weirdfoo.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/weirdfoo.solc deleted file mode 100644 index a94677e9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/weirdfoo.solc +++ /dev/null @@ -1,5 +0,0 @@ -data W(a) = W(a); -class a: Foo {function foo(); } -instance ((word, a) : Foo) => (word, W(a)) : Foo { - function foo() {} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/withdraw.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/withdraw.solc deleted file mode 100644 index 9bb5746d..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/withdraw.solc +++ /dev/null @@ -1,20 +0,0 @@ -import IndexLib; - -contract Uint { - reserved : word; - msg_sender : address; // mock msg.sender - owner : address; - decimals : uint; - totalSupply : uint; - balances : mapping(address,uint); - - function withdraw(src, amt) { - balances[src] = Num.sub(balances[src], amt); - } - - - function main() { - withdraw(msg_sender, totalSupply); - return balances[msg_sender] : uint; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/word-match.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/word-match.solc deleted file mode 100644 index 4862bb72..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/word-match.solc +++ /dev/null @@ -1,11 +0,0 @@ - -forall a . class a:IsWord { function toWord(x : a) -> word; } - -function kw(a:word, b:word) {return a;} - -forall a b . a:IsWord, b:IsWord -=> function bar(x:(a,b)) -> word { - match x { - | (t,u) => return kw(IsWord.toWord(t), IsWord.toWord(u)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/xref.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/xref.solc deleted file mode 100644 index a0d131f9..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/xref.solc +++ /dev/null @@ -1,133 +0,0 @@ -function add_(x:word, y:word) { // _add is not a legal identifier :( - let res: word; - assembly { - res := add(x, y) - } - return res; - } - -function mload_(x:word) -> word { - let res: word; - assembly { - res := mload(x) - } - return res; - } - -function mstore_(a:word, v:word) { - assembly { mstore(a,v) } -} - -forall r d . class r:Ref(d) { function load(x:r) -> d; function store(x:r, v:d) -> ();} - -forall self underlyingType . class self:Typedef(underlyingType) { - function rep(x:self) -> underlyingType; // abbr: x.rep = Typedef.rep(x) - function abs(x:underlyingType) -> self; // abbr: x.abs -} -data Proxy(a) = Proxy; - -data M(a) = M(word); - -forall a . instance M(a) : Typedef(word) { - function rep(m : M(a)) -> word { match m { | M(w) => return w; } } - function abs(w : word) -> M(a) { return M(w); } -} - -forall Self . class Self:MemoryType { - function memorySize(p:Proxy(Self)) -> word; - /* inline function sizeof(Self) -> word { // an abbreviation to avoid writing Proxy; wasteful unless inlined - return memorySize(Proxy:Proxy(self)); - } */ - function memoryStep(word, self:Self) -> word; - function mload(r:word) -> Self; - function mstore(r:word, v:Self) -> (); -} - -forall Self . Self:MemoryType => function sizeof(self:Self) -> word { - return MemoryType.memorySize(Proxy:Proxy(Self)); -} - -forall a d . class a:MemoryRef(d) { function addr(r:a) -> word; } -forall a . instance M(a):MemoryRef(a) { function addr(r:M(a)) -> word {return Typedef.rep(r);} } - -forall a . function xaddr(r:M(a)) -> word { return MemoryRef.addr(r); } -forall a b . function asMemRefTo(r:M(a), p:Proxy(b)) -> M(b) { return Typedef.abs(xaddr(r)); } - -forall a . a:MemoryType => function stepStore(aa: word, va: a) -> word { - MemoryType.mstore(aa, va); - return add_(aa, MemoryType.memorySize(Proxy:Proxy(a))); -} - -forall Self r . Self:MemoryType, r:MemoryRef(Self) => instance r : Ref(Self) { - function load(r:M(Self)) -> Self { return MemoryType.mload(xaddr(r)); } - function store(r:M(Self), v:Self) -> () { MemoryType.mstore(xaddr(r), v); } -} - -instance word:MemoryType { - function memorySize(p:Proxy(word)) -> word { return 32; } - function memoryStep(a:word, self:word) -> word { return add_(a,32); } - function mload(a: word) -> word { return mload_(a); } - function mstore(a: word, v:word) -> () { mstore_(a, v); } -} - -forall a b . a:MemoryType, b:MemoryType => instance (a,b) : MemoryType { - function memorySize(p:Proxy((a,b))) -> word { - return add_(MemoryType.memorySize(Proxy:Proxy(a)), MemoryType.memorySize(Proxy:Proxy(a)) ); - } - - function mload(aa:word) -> (a,b) { - let va = MemoryType.mload(aa); - let ab = add_(aa, sizeof(va)); - let vb = MemoryType.mload(ab); - return (va,vb); - } - - function mstore(aa:word, v: (a,b)) -> () { - match v { | pair(va, vb) => mstore2(aa, va, vb); } // match-compiler cannot compile mopre than 1 stmt in a branch :( - } -} - -forall a b . a: MemoryType, b: MemoryType => function mstore2(aa:word, va:a, vb: b) { //needed because of bug in match-compiler - let ab = stepStore(aa, va); - MemoryType.mstore(ab, vb); -} - -pragma no-bounded-variable-condition MemoryRef; -pragma no-coverage-condition Ref; -pragma no-patterson-condition Ref; -data XRef(st, field, fieldType) = XRef(st, field); -data PairFst = PairFst; -data PairSnd = PairSnd; - - -forall a b r . r:MemoryRef ( (a,b)), a:MemoryType, b:MemoryType => instance XRef(r, PairFst, a) : MemoryRef(a) { - function addr(xr : XRef(r, PairFst, a)) -> word { - match xr { | XRef(r, _) => return MemoryRef.addr(r); } - } -} - -forall a b r . r:MemoryRef ((a,b)), a:MemoryType, b:MemoryType => instance XRef(r, PairSnd, b) : MemoryRef(b) { - function addr(xr : XRef (r, PairSnd, b)) -> word { - match xr { - | XRef(r, _) => return add_(MemoryRef.addr(r), MemoryType.memorySize(Proxy : Proxy(b))); - } - } -} - -contract Ref219 { - function main() { - let mp:M((word, word, word)) = M(96); // no alloc yet - let p = (1,16,25); - Ref.store(mp, p); - - let ra = XRef(mp, PairFst); - let a = Ref.load(ra); - let r2 = XRef(mp, PairSnd); - let rb = XRef(r2, PairFst); - let a = Ref.load(ra); - let b = Ref.load(rb); - let rc = XRef(r2, PairSnd); - let c = Ref.load(rc); - return add_(a, add_(b, c)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-for.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-for.solc deleted file mode 100644 index f978b196..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-for.solc +++ /dev/null @@ -1,14 +0,0 @@ -contract YulFor { - function main() { - let loopStart = 128; - let loopEnd = 256; - let res : word; - assembly { - let i := loopStart - for {} lt(i, loopEnd) { i := add(i, 32) } - { mstore(i, 42) } - res := mload(192) - } - return res; - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-function-typing.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-function-typing.solc deleted file mode 100644 index 32812c24..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-function-typing.solc +++ /dev/null @@ -1,8 +0,0 @@ -function foo(length:word, pos:word) -> word { - let ret: word; - assembly { - // ret := add(pos, mul(0x20, iszero(iszero(length)))) - ret := iszero(iszero(length)) - } - return ret; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-return.solc b/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-return.solc deleted file mode 100644 index 95d1631b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/cases/yul-return.solc +++ /dev/null @@ -1,7 +0,0 @@ -contract C { - function main() -> () { - assembly { - return(0,0); - } - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/coverage.solc b/crates/parser/tests/fixtures/ok/solcore_examples/coverage.solc deleted file mode 100644 index c412dc91..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/coverage.solc +++ /dev/null @@ -1,8 +0,0 @@ -pragma no-coverage-condition ; - -data List(a) = Nil | Cons(a,List(a)); -data Bool = True | False ; - -forall a b c . class a : C(b,c) {} - -forall a b . instance List(b) : C (a, List(a)) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/basic.solc b/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/basic.solc deleted file mode 100644 index 76ef57cf..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/basic.solc +++ /dev/null @@ -1,17 +0,0 @@ -import dispatch; - -contract C { - function nothing() -> () {} - - function something() -> (uint256) { - return uint256(1); - } - - function add2(x : uint256, y : uint256) -> uint256 { - return Add.add(x,y); - } - - function add3(x : uint256, y : uint256, z : uint256) -> uint256 { - return Add.add(z, Add.add(x,y)); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/miniERC20.solc b/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/miniERC20.solc deleted file mode 100644 index 18ce3d89..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/miniERC20.solc +++ /dev/null @@ -1,107 +0,0 @@ -import std; -import dispatch; - -function caller() -> address { - let res: word; - assembly { - res := caller() - } - return address(res); -} - -function myrevert(msg: word) -> () { - assembly { mstore(0, msg) revert(0, 32) } -} - -function require(cond: bool, msg: word ) { - if( !cond ) { myrevert(msg); } -} - -contract MiniERC20 { - name : string; - symbol : string; - owner : address; - decimals : uint256; // should be uint8 when we get to it - totalSupply : uint256; - balances : mapping(address,uint256); - allowance : mapping(address, mapping(address, uint256)); - - constructor(name_ : memory(string), symbol_ : memory(string), totalSupply_:uint256) { - name = name_; - symbol = symbol_; - owner = caller(); - decimals = uint256(18); - mint(totalSupply_); - } - - function name() -> memory(string) { - return name; - } - - function symbol() -> memory(string) { - return symbol; - } - - function decimals() -> uint256 { - return decimals; - } - - function allowance(owner_ : address, spender: address) -> uint256 { - return allowance[owner_][spender]; // don't use "owner" here - } - - function balanceOf(account : address) -> uint256 { - return balances[account]; - } - - function totalSupply() -> uint256 { - return totalSupply; - } - - function mint(amount:uint256) -> () { - balances[owner] = Num.add(balances[owner], amount); - totalSupply = Num.add(totalSupply, amount); - } - - function transfer(dst : address, amt : uint256) -> bool { - return transferFrom(caller(), dst, amt); - } - - function transferFrom(src:address, dst:address, amt:uint256) -> bool { - let msg_sender = caller(); - require( balances[src] >= amt /* "token/insufficient-balance" */ - , 0x746f6b656e2f696e73756666696369656e742d62616c616e6365 - ); - - if (src != msg_sender && allowance[src][msg_sender] != (Num.maxVal():uint256)) { - require( allowance[src][msg_sender] >= amt /* "token/insufficient-allowance" */ - , 0x746f6b656e2f696e73756666696369656e742d616c6c6f77616e6365 - ); - allowance[src][msg_sender] -= amt; - } - balances[src] = balances[src] - amt; - balances[dst] = balances[dst] + amt; - // emit Transfer(src, dst, amt); - return true; - } - - function approve(usr: address, amt: uint256) -> bool { - let msg_sender = caller(); - allowance[msg_sender][usr] = amt; - // emit Approval(msg.sender, usr, amt); - return true; - } - - - // testing - function getMyBalance() -> uint256 { - return balances[caller()]; - } - - function test() -> uint256 { - approve(address(0), uint256(10)); - transferFrom(caller(), address(0), uint256(958)); - return getMyBalance(); - } - -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/stringid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/stringid.solc deleted file mode 100644 index 972bcc30..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/dispatch/stringid.solc +++ /dev/null @@ -1,41 +0,0 @@ -import dispatch; - -contract C { - constructor() {} - function id(x:memory(string)) -> (memory(string)) { - let ptr : word = Typedef.rep(x); - let len : word; - let n1 : word; - assembly { - len := mload(ptr) - n1 := mload(add(ptr,32)) - } - log1(len, 0xc001); - log1(n1, 0xc002); - - return x; - } - - function const_a() -> (memory(string)) { - let resPtr = allocate_memory(64); - let payload = 0x7777777777777777777777777777777777777777777777777777777777777777; - mstore(resPtr, 3); - mstore(resPtr+32, payload); - return memory(resPtr); - } - function mylen(x:memory(string)) -> uint256 { - let ptr : word = Typedef.rep(x); - let l : word; - let n1 : word; - assembly { - l := mload(ptr) - n1 := mload(add(ptr,32)) - } - // log1(l, 0xc001); - // log1(n1, 0xc002); - - return uint256(l); - } - - // function answer() -> uint256 { return uint256(17); } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/imports/booldef.solc b/crates/parser/tests/fixtures/ok/solcore_examples/imports/booldef.solc deleted file mode 100644 index 8f498744..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/imports/booldef.solc +++ /dev/null @@ -1,20 +0,0 @@ -data Bool = True | False; - -function not (b : Bool) -> Bool { - match b { - | True => return False; - | False => return True; - } -} - -forall a . class a : C { - function c (x : a, y : a) -> word ; -} - -forall a . class a : D { - function d() -> a ; -} - -forall a . a : C, a : D => function id (x : a) -> word { - return C.c(x, D.d()); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/imports/boolmain.solc b/crates/parser/tests/fixtures/ok/solcore_examples/imports/boolmain.solc deleted file mode 100644 index c43efd69..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/imports/boolmain.solc +++ /dev/null @@ -1,5 +0,0 @@ -import booldef ; - -function and (b1 : Bool, b2 : Bool) -> Bool { - return False ; -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/021nid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/021nid.solc deleted file mode 100644 index baeb8224..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/021nid.solc +++ /dev/null @@ -1,15 +0,0 @@ -contract Id1 { - function id(x) { - return x ; - } - - function nid() { - return id; - } - - function const(x, y) { return x; } - - function main() { - return nid(42); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/022nid-invoke.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/022nid-invoke.solc deleted file mode 100644 index 628cb016..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/022nid-invoke.solc +++ /dev/null @@ -1,37 +0,0 @@ - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; - } - - function id(x) { - return x ; - } - - data IdToken(a) = IdToken; - -instance IdToken(a) : Invokable(a,a) { - function invoke(token: IdToken(a), arg:a) -> a { - return id(arg); - } -} - -contract InvokeId { - function id(x) { - return x ; - } - - /* - function nid() { - return id; - } - */ - - function nidimpl() { - return IdToken; - } - - function main() { - // Instead of: `return nid(42)` - return invoke(nidimpl(), 42); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/024lamid.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/024lamid.solc deleted file mode 100644 index b399e929..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/024lamid.solc +++ /dev/null @@ -1,11 +0,0 @@ -contract Id1 { - function id(x) { - return x ; - } - - - function main() { - let nid = lam(x) {return x;}; - return nid(42); - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/025lamid-invoke.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/025lamid-invoke.solc deleted file mode 100644 index 91e37fe7..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/025lamid-invoke.solc +++ /dev/null @@ -1,30 +0,0 @@ -/* Manual translation of: -contract Id1 { - function main() { - let nid = lam(x) {return x;}; - return nid(42); - } -} -*/ - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; -} - -function lam0impl(x: c) -> c { return x; } - -data Lam0Token(a) = Lam0Token; - -instance Lam0Token(a) : Invokable(a,a) { - function invoke(token: Lam0Token(a), arg:a) -> a { - return lam0impl(arg); - } -} - - -contract InvokeLam { -function main() { - let nid = Lam0Token; - return invoke(nid, 42); -} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/026capture.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/026capture.solc deleted file mode 100644 index 3fbc3684..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/026capture.solc +++ /dev/null @@ -1,47 +0,0 @@ -/* Manual translation of: -contract Id1 { - function main() { - let y = 42; - let nid = lam(x) {return addW(x,y);}; - return nid(17); - } -} -*/ - -function addW(x: Word, y:Word) -> Word { - let res : Word; - assembly { - res := add(x, y) - } - return res; -} - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; -} - -// env might be a tuple, here it is a single Word -function lam1impl(env: Word, x: c) -> c { - let y = env; - return addW(x,y); -} - -data Lam1Closure(a) = Lam1Closure(Word); - -instance Lam1Closure(a) : Invokable(a,Word) { - function invoke(clos: Lam1Closure(a), arg:a) -> Word { - match clos { - | Lam1Closure(env) => return lam1impl(env, arg); - } - } -} - - -contract InvokeCapLam { -function main() { - let y = 42; - let clos = Lam1Closure(y); - - return invoke(clos, 17); -} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/027retfun.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/027retfun.solc deleted file mode 100644 index aa7cc9e8..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/027retfun.solc +++ /dev/null @@ -1,44 +0,0 @@ -/* Manual translation of: -contract Id1 { - - function foo() { - let y = 42; - let nid = lam(x) {return y;}; - return nid; - } - function main() { - return nid(17); - } -} -*/ - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; -} - -// env might be a tuple, here it is a single Word -function lam1impl(env: Word, x: c) -> c { return env; } - -data Lam1Closure(a) = Lam1Closure(Word); - -instance Lam1Closure(a) : Invokable(a,Word) { - function invoke(clos: Lam1Closure(a), arg:a) -> Word { - match clos { - | Lam1Closure(env) => return lam1impl(env, arg); - } - } -} - - -contract InvokeCapLam { -function foo() { - let y = 42; - let clos = Lam1Closure(y); - return clos; -} - -function main() { - - return invoke(foo(), 17); -} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/028modifier.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/028modifier.solc deleted file mode 100644 index 0020e9a2..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/028modifier.solc +++ /dev/null @@ -1,87 +0,0 @@ -function add1(x) { - return addW(x,1); -} - -function addW(x: Word, y:Word) -> Word { - let res : Word; - assembly { - res := add(x, y) - } - return res; -} - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; -} - - -/* Manual translation of: -contract Id1 { - // modifier calls its argument and adds one to result - function add1mod(f) { - return lam(a) { return add1(f(a)); } - } - - function foo(x) { - return addW(x,2); - } - - function main() { - let bar = add1mod(foo); - return bar(39); - } -} -*/ - -function foo(x:Word) -> Word { - return addW(x, 2); -} - -data FooToken = FooToken; - -instance FooToken:Invokable(Word, Word) { - function invoke(self:FooToken, arg: Word) -> Word { - return foo(arg); - } -} - -// lambda in add1mod captures a function -// so env contains the closure - -forall f.(f: Invokable(Word,Word)) => function lam1impl (env : f, a:Word) { - let f = env; - return add1(invoke(f, a)); -} - -// we want: -// data Lam1Closure = f:Invokable(Word,Word) => Lam1Closure(f) - -data Lam1Closure(f) = Lam1Closure(f); - -/* -function extractEnv(clos: Lam1Closure(f)) -> f { - match clos { - | Lam1Closure(env) => return env; - } -} -*/ -instance (f:Invokable(Word,Word)) => Lam1Closure(f) : Invokable(Word,Word) { - function invoke(clos, arg:Word) -> Word { - match clos { - | Lam1Closure(env) => return lam1impl(env, arg); - } - } -} - -function add1mod(f) { - return Lam1Closure(f); -} - -contract Modifier { - - -function main() { - let barClos = add1mod(FooToken); - return invoke(barClos, 39); -} -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/031enum.solc b/crates/parser/tests/fixtures/ok/solcore_examples/invokable/031enum.solc deleted file mode 100644 index 1f2d520a..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/invokable/031enum.solc +++ /dev/null @@ -1,59 +0,0 @@ -function addW(x: Word, y:Word) -> Word { - let res : Word; - assembly { - res := add(x, y) - } - return res; -} - -class a:Enum { - function fromEnum(x:a) -> Word; - } - - data Color = R | G | B; - -instance Color : Enum { - function fromEnum(c) { - match c { - | R => return 1; - | G => return 2; - | B => return 3; - } - } -} - -data Bool = False | True; - -instance Bool : Enum { - function fromEnum(b) { - match b { - | False => return 0; - | True => return 1; - } - } -} -data FromEnumToken(a) = FromEnumToken; - -class self : Invokable(args, ret) { - function invoke (s:self, a:args) -> ret; -} - -instance (a:Enum) => FromEnumToken(a) : Invokable(a,Word) { - function invoke(fet : FromEnumToken(a), arg) -> Word { - return fromEnum(arg); - } -} -contract RGB { - function main() { - /* - let x = fromEnum(B); - let y = fromEnum(True); - */ - - let fetC = FromEnumToken; - let fetB = FromEnumToken; - let x = invoke(fetC, B); - let y = invoke(fetB,True); - return addW(x,y); - } -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/patterson.solc b/crates/parser/tests/fixtures/ok/solcore_examples/patterson.solc deleted file mode 100644 index 9293a12b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/patterson.solc +++ /dev/null @@ -1,17 +0,0 @@ -pragma no-patterson-condition ; - -forall self . class self:A {} -forall self . class self:B {} -forall self . class self:C {} -forall self . class self:D {} - - -data Uint256 = U; -data T(x) = T; -data S(x) = T; - -// This works. -forall U . U : A => instance T(U):D {} - -// This should also work, but reports a violation of the Paterson condition. -forall U . U : A, U : B, U : C => instance S(U):D {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/bound.solc b/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/bound.solc deleted file mode 100644 index 827524e4..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/bound.solc +++ /dev/null @@ -1,16 +0,0 @@ -pragma no-bounded-variable-condition F; - -forall a . class a:D { function f(x:a); } -forall a b . class a:F(b) {} - -data Memory(a) = Memory(word); - -forall a . instance Memory(a):F(Memory(Memory(Memory(a)))) {} -forall a c . instance (c:D,a:F(c)) => Memory(Memory(Memory(a))):D { - function f(x:Memory(Memory(Memory(a)))) {} -} - -forall b . function g(y:b) { - let x : Memory(Memory(Memory(Memory(b)))); - f(x); -} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/coverage.solc b/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/coverage.solc deleted file mode 100644 index c412dc91..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/coverage.solc +++ /dev/null @@ -1,8 +0,0 @@ -pragma no-coverage-condition ; - -data List(a) = Nil | Cons(a,List(a)); -data Bool = True | False ; - -forall a b c . class a : C(b,c) {} - -forall a b . instance List(b) : C (a, List(a)) {} diff --git a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/patterson.solc b/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/patterson.solc deleted file mode 100644 index 9293a12b..00000000 --- a/crates/parser/tests/fixtures/ok/solcore_examples/pragmas/patterson.solc +++ /dev/null @@ -1,17 +0,0 @@ -pragma no-patterson-condition ; - -forall self . class self:A {} -forall self . class self:B {} -forall self . class self:C {} -forall self . class self:D {} - - -data Uint256 = U; -data T(x) = T; -data S(x) = T; - -// This works. -forall U . U : A => instance T(U):D {} - -// This should also work, but reports a violation of the Paterson condition. -forall U . U : A, U : B, U : C => instance S(U):D {} diff --git a/crates/parser/tests/fixtures/ok/spec/00answer.solc b/crates/parser/tests/fixtures/ok/spec/00answer.solc deleted file mode 100644 index f7112655..00000000 --- a/crates/parser/tests/fixtures/ok/spec/00answer.solc +++ /dev/null @@ -1,5 +0,0 @@ -contract Answer { - function main() { - return 42; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/spec/010answer.solc b/crates/parser/tests/fixtures/ok/spec/010answer.solc deleted file mode 100644 index f7112655..00000000 --- a/crates/parser/tests/fixtures/ok/spec/010answer.solc +++ /dev/null @@ -1,5 +0,0 @@ -contract Answer { - function main() { - return 42; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/spec/011id.solc b/crates/parser/tests/fixtures/ok/spec/011id.solc deleted file mode 100644 index a4ecc489..00000000 --- a/crates/parser/tests/fixtures/ok/spec/011id.solc +++ /dev/null @@ -1,14 +0,0 @@ -contract Id1 { - - data Bool = False | True; - - function id(x) { - return x ; - } - - function const(x, y) { return x; } - - function main() { - return const(id(42), False); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/012nid.solc b/crates/parser/tests/fixtures/ok/spec/012nid.solc deleted file mode 100644 index 16b629fb..00000000 --- a/crates/parser/tests/fixtures/ok/spec/012nid.solc +++ /dev/null @@ -1,15 +0,0 @@ -contract Id1 { - function id(x) { - return x ; - } - - function nid() { - return id; - } - - function const(x, y) { return x; } - - function main() { - return const(nid(42), id(1)); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/013comp.solc b/crates/parser/tests/fixtures/ok/spec/013comp.solc deleted file mode 100644 index 2ce169c2..00000000 --- a/crates/parser/tests/fixtures/ok/spec/013comp.solc +++ /dev/null @@ -1,16 +0,0 @@ -contract Compose { - function compose(f,g) { - return lam (x) { - return f(g(x)); - } ; - } - - function id(x) { return x; } - - function idid() { return compose(id,id); } - - function main() { - let f = compose(id,id); - return f(42); - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/spec/01id.solc b/crates/parser/tests/fixtures/ok/spec/01id.solc deleted file mode 100644 index a4ecc489..00000000 --- a/crates/parser/tests/fixtures/ok/spec/01id.solc +++ /dev/null @@ -1,14 +0,0 @@ -contract Id1 { - - data Bool = False | True; - - function id(x) { - return x ; - } - - function const(x, y) { return x; } - - function main() { - return const(id(42), False); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/021not.solc b/crates/parser/tests/fixtures/ok/spec/021not.solc deleted file mode 100644 index 053e9a6a..00000000 --- a/crates/parser/tests/fixtures/ok/spec/021not.solc +++ /dev/null @@ -1,21 +0,0 @@ -contract Not { - data Bool = False | True; - - function main() { - return fromBool(bnot(False)); - } - - function fromBool(b) { - match(b) { - | False => return 0; - | True => return 1; - } - } - - function bnot(b) { - match b { - | False => return True; - | True => return False; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/022add.solc b/crates/parser/tests/fixtures/ok/spec/022add.solc deleted file mode 100644 index 202a0821..00000000 --- a/crates/parser/tests/fixtures/ok/spec/022add.solc +++ /dev/null @@ -1,13 +0,0 @@ -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -contract Add1 { - function main() { - return add(40, 2); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/024arith.solc b/crates/parser/tests/fixtures/ok/spec/024arith.solc deleted file mode 100644 index d462064c..00000000 --- a/crates/parser/tests/fixtures/ok/spec/024arith.solc +++ /dev/null @@ -1,64 +0,0 @@ - - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -function sub(x : word, y : word) { - let res: word; - assembly { - res := sub(x, y) - } - return res; -} - -function div(x : word, y: word) { - let res: word; - assembly { - res := div(x, y) - } - return res; -} - -function sdiv(x : word, y: word) { - let res: word; - assembly { - res := sdiv(x, y) - } - return res; -} - -function mod(x : word, y: word) { - let res: word; - assembly { - res := mod(x, y) - } - return res; -} - -function smod(x : word, y: word) { - let res: word; - assembly { - res := smod(x, y) - } - return res; -} - -function exp(x : word, y: word) { - let res: word; - assembly { - res := exp(x, y) - } - return res; -} - - -contract Arith { - function main() { - return add(mod(sub(div(exp(2,18),4), 1), 16), 27); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/027sstore.solc b/crates/parser/tests/fixtures/ok/spec/027sstore.solc deleted file mode 100644 index b1006dfc..00000000 --- a/crates/parser/tests/fixtures/ok/spec/027sstore.solc +++ /dev/null @@ -1,10 +0,0 @@ -contract Sstore { - function main() { - let res : word; - assembly { - sstore(0, 42) - res := sload(0) - } - return res; - } -} \ No newline at end of file diff --git a/crates/parser/tests/fixtures/ok/spec/02nid.solc b/crates/parser/tests/fixtures/ok/spec/02nid.solc deleted file mode 100644 index d4633bbf..00000000 --- a/crates/parser/tests/fixtures/ok/spec/02nid.solc +++ /dev/null @@ -1,16 +0,0 @@ -contract Id1 { - function id(x) { - return x ; - } - - function nid() { - return id; - } - - function const(x, y) { return x; } - - function main() { - let f = nid(); - return const(f(42), id(1)); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/031maybe.solc b/crates/parser/tests/fixtures/ok/spec/031maybe.solc deleted file mode 100644 index ff7f679e..00000000 --- a/crates/parser/tests/fixtures/ok/spec/031maybe.solc +++ /dev/null @@ -1,16 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function main() { - return maybe(0, Some(42)); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/032simplejoin.solc b/crates/parser/tests/fixtures/ok/spec/032simplejoin.solc deleted file mode 100644 index 47d17cf7..00000000 --- a/crates/parser/tests/fixtures/ok/spec/032simplejoin.solc +++ /dev/null @@ -1,35 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - - function join(mmx) { - match mmx { - | None => return None; - | Some(None) => return None; - | Some(Some(x)) => return Some(x); - } - } - - function join2(mmx) { - match mmx { - | Some(m) => match m { - | None => return None; - | Some(x) => return Some(x); - } - | _ => return None; - } - } - - function main() { - return maybe(0, join(Some(Some(42)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/033join.solc b/crates/parser/tests/fixtures/ok/spec/033join.solc deleted file mode 100644 index a78be8a5..00000000 --- a/crates/parser/tests/fixtures/ok/spec/033join.solc +++ /dev/null @@ -1,23 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - match mmx { - | Some(Some(x)) => return Some(x); - | _ => return None; - } - } - - function main() { - return maybe(0, join(Some(Some(42)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/034cojoin.solc b/crates/parser/tests/fixtures/ok/spec/034cojoin.solc deleted file mode 100644 index c77f07f9..00000000 --- a/crates/parser/tests/fixtures/ok/spec/034cojoin.solc +++ /dev/null @@ -1,38 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - let result = None; - match mmx { - | Some(Some(x)) => result = Some(x); - | None => result = None; - } - return result; - } - - function extract(mx) { - match mx { - | Some(x) => return x; - } - } - - function cojoin(x) { // Test that sum types can grow - let result = None; - result = Some(x); - return result; - } - - - function main() { - return maybe(0, join(cojoin(Some(42)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/035padding.solc b/crates/parser/tests/fixtures/ok/spec/035padding.solc deleted file mode 100644 index 568c8aae..00000000 --- a/crates/parser/tests/fixtures/ok/spec/035padding.solc +++ /dev/null @@ -1,14 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function maybe(n, o) { - match o { - | Some(x) => return x; - | None => return n; - } - } - - function main() { - return maybe(7, None); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/036wildcard.solc b/crates/parser/tests/fixtures/ok/spec/036wildcard.solc deleted file mode 100644 index 38de9ba5..00000000 --- a/crates/parser/tests/fixtures/ok/spec/036wildcard.solc +++ /dev/null @@ -1,14 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function maybe(n, o) { - match o { - | Some(x) => return x; - | _ => return n; - } - } - - function main() { - return maybe(7, None); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/037dwarves.solc b/crates/parser/tests/fixtures/ok/spec/037dwarves.solc deleted file mode 100644 index 8c12d410..00000000 --- a/crates/parser/tests/fixtures/ok/spec/037dwarves.solc +++ /dev/null @@ -1,16 +0,0 @@ -contract Dwarves { - data Dwarf = Doc | Grumpy | Sleepy | Bashful | Happy | Sneezy | Dopey; - - - function fromEnum(c) { - match c { - | Doc => return 1; - | Grumpy => return 2; - | Sleepy => return 3; - | Bashful => return 4; - | Happy => return 5; - } - } - - function main() { return fromEnum(Happy); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/038food0.solc b/crates/parser/tests/fixtures/ok/spec/038food0.solc deleted file mode 100644 index a3340676..00000000 --- a/crates/parser/tests/fixtures/ok/spec/038food0.solc +++ /dev/null @@ -1,23 +0,0 @@ -data Food = Curry | Beans | Other; -data CFood = Red(Food) | Green(Food) | Nocolor; - - - - function fromEnum(x : CFood) { - match x { - | Red(Curry) => return 1; - | Green(Beans) => return 42; - | _ => return 3; - } - } - - -contract Food { - function id(x) { - return(x); - } - - function main() { - return fromEnum(id(Green(Beans))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/039food.solc b/crates/parser/tests/fixtures/ok/spec/039food.solc deleted file mode 100644 index 225a9bd5..00000000 --- a/crates/parser/tests/fixtures/ok/spec/039food.solc +++ /dev/null @@ -1,29 +0,0 @@ - -data Food = Curry | Beans | Other; -data CFood = Red(Food) | Green(Food) | Nocolor; - - - - - function fromEnum(x : Food) { - match x { - | Curry => return 1; - | Beans => return 42; - | Other => return 3; - } - } - - -contract Food { - function eat(x) { - match x { - | Red(f) => return f; - | Green(f) => return f; - | _ => return Other; - } - } - - function main() { - return fromEnum(eat(Green(Beans))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/041pair.solc b/crates/parser/tests/fixtures/ok/spec/041pair.solc deleted file mode 100644 index 41a414bc..00000000 --- a/crates/parser/tests/fixtures/ok/spec/041pair.solc +++ /dev/null @@ -1,12 +0,0 @@ -contract Pair { - - function fst(p) { - match p { - | (a,b) => return a; - } - } - - function main() { - return fst((1,0)); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/042triple.solc b/crates/parser/tests/fixtures/ok/spec/042triple.solc deleted file mode 100644 index a15ba502..00000000 --- a/crates/parser/tests/fixtures/ok/spec/042triple.solc +++ /dev/null @@ -1,12 +0,0 @@ -contract Triple { - - function asel(t) { - match t { - | (a,b,c) => return c; - } - } - - function main() { - return asel((1,21,42)); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/043fstsnd.solc b/crates/parser/tests/fixtures/ok/spec/043fstsnd.solc deleted file mode 100644 index a4074b23..00000000 --- a/crates/parser/tests/fixtures/ok/spec/043fstsnd.solc +++ /dev/null @@ -1,33 +0,0 @@ - - -data B = F | T; -data Pair(a,b) = Pair(a,b); - -function fst (p) { - match p { - | Pair(x,y) => return x; - } -} - -function snd(p) { - match p { - | Pair(x,y) => return y; - } -} - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - - -function addPair(p) { - return add(fst(p), snd(p)); -} - -contract FstSnd { - function main() { return addPair(Pair(41,1)); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/047rgb.solc b/crates/parser/tests/fixtures/ok/spec/047rgb.solc deleted file mode 100644 index f54e427b..00000000 --- a/crates/parser/tests/fixtures/ok/spec/047rgb.solc +++ /dev/null @@ -1,10 +0,0 @@ -contract RGB { - data Color = R | G | B; - function main() { - match B { - | R => return 4; - | G => return 2; - | B => return 42; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/048rgb2.solc b/crates/parser/tests/fixtures/ok/spec/048rgb2.solc deleted file mode 100644 index c75b4880..00000000 --- a/crates/parser/tests/fixtures/ok/spec/048rgb2.solc +++ /dev/null @@ -1,13 +0,0 @@ -contract RGB { - data Color = R | G | B; - - function fromEnum(c) { - match c { - | R => return 4; - | G => return 2; - | B => return 42; - } - } - - function main() { return fromEnum(B); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/051expreturn.solc b/crates/parser/tests/fixtures/ok/spec/051expreturn.solc deleted file mode 100644 index 502a5d70..00000000 --- a/crates/parser/tests/fixtures/ok/spec/051expreturn.solc +++ /dev/null @@ -1,57 +0,0 @@ -data Bool = False | True; -data W = W(Word); -data U = U; - -// empty class needed since forall expects a nonempty context -class a :Top {} -instance a:Top {} - -/* For experiments, special handling when emitting code */ -// this does not work, typechecker forces a ~ b -// forall a, b.(a:Top, b:Top) => function ereturn(x:a) -> b { let res: b; return res; } -// we might have -// forall a.(a:Top) => function ereturn(x:a) -> a -// or - -forall a . function ereturn(x:a) -> Unit { let res: Unit; return res; } -// and then cast it to any type using unsafeCast - -/* simulate match expression - x = match { | False => return 77; | True => W(22) } -*/ -function elimBool1(b:Bool) -> Word { - let x : W; - x = W(1); - match b { - // this works - // | False => x = unsafeCast(ereturn(77)); - // but this does not - unknown intermediate type - // | False => x = unsafeCast(unsafeCast(ereturn(77))); - // what about "return(return 77)"? - // this works - | False => x = unsafeCast(ereturn(ereturn(77))); - // but this does not - // | False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); - | True => x = W(22); - } - - match x { - | W(y) => return y; - } - -} - -// "semicolon" -forall a. function semi(x:a) -> U { return U;} - -forall a b. function unsafeCast(x:a) -> b { - let res: b; return res; -} - - -contract ExpReturn { - function main() -> Word { - return elimBool1(False); - // return elimBool1(False); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/051negBool.solc b/crates/parser/tests/fixtures/ok/spec/051negBool.solc deleted file mode 100644 index 26319342..00000000 --- a/crates/parser/tests/fixtures/ok/spec/051negBool.solc +++ /dev/null @@ -1,29 +0,0 @@ - -class a : Neg { - function neg(x:a) -> a; -} - -data B = F | T; - - -instance B : Neg { - function neg (x : B) { - match x { - | F => return T; - | T => return F; - } - } -} - - -contract NegBool { - - function fromB(b) { - match b { - | F => return 0; - | T => return 1; - } - } - - function main() { return fromB(Neg.neg(F)); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/052negPair.solc b/crates/parser/tests/fixtures/ok/spec/052negPair.solc deleted file mode 100644 index 3d3542d0..00000000 --- a/crates/parser/tests/fixtures/ok/spec/052negPair.solc +++ /dev/null @@ -1,63 +0,0 @@ - -class a : Neg { - function neg(x:a) -> a; -} - -data B = F | T; -data Pair(a,b) = Pair(a,b); - -instance B : Neg { - function neg (x : B) { - match x { - | F => return T; - | T => return F; - } - } -} - -function fst (p) { - match p { - | Pair(x,y) => return x; - } -} - -function snd(p) { - match p { - | Pair(x,y) => return y; - } -} - - -instance (a:Neg,b:Neg) => Pair(a,b):Neg { - function neg(p) { - return Pair(Neg.neg (fst(p)), Neg.neg(snd (p))); - } -} - -/* -instance (a:Neg,b:Neg) => Pair(a,b):Neg { - function neg(p) { - match p { - | Pair(a,b) => return Pair(neg(a), neg(b)); - } - } -} -*/ -contract NegPair { - - function bnot(x) { - match x { - | T => return F; - | F => return T; - } -} - - function fromB(b) { - match b { - | F => return 0; - | T => return 1; - } -} - - function main() { return fromB(fst(Neg.neg(Pair(F,T)))); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/052return.solc b/crates/parser/tests/fixtures/ok/spec/052return.solc deleted file mode 100644 index ccedfcec..00000000 --- a/crates/parser/tests/fixtures/ok/spec/052return.solc +++ /dev/null @@ -1,57 +0,0 @@ -data Bool = False | True; -data W = W(word); -data U = U; - - -/* For experiments, special handling when emitting code */ -// this does not work, typechecker forces a ~ b -// function ereturn(x:a) -> b { let res: b; return res; } -// we might have -// function ereturn(x:a) -> a -// or - -function ereturn(x:a) -> unit { let res: unit; return res; } -// and then cast it to any type using unsafeCast - -/* simulate match expression - x = match { | False => return 77; | True => W(22) } -*/ -function elimBool1(b:Bool) -> word { - let x : W; - x = W(1); - match b { - // this works - | False => x = unsafeCast(ereturn(77)); - // but this does not - unknown intermediate type - // | False => x = unsafeCast(unsafeCast(ereturn(77))); - // what about "return(return 77)"? - // this does not work - // | False => x = ereturn(ereturn(77)); - // this works - // | False => x = unsafeCast(ereturn(ereturn(77))); - // this does not work (monomorphisation fails): - // | False => x = unsafeCast(ereturn(unsafeCast(ereturn(77)))); - - | True => x = W(22); - } - - match x { - | W(y) => return y; - } - -} - -// "semicolon" -function semi(x:a) -> U { return U;} - -function unsafeCast(x:a) -> b { -let res: b; return res; -} - - -contract ExpReturn { - function main() -> word { - return elimBool1(False); - // return elimBool1(True); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/053return.solc b/crates/parser/tests/fixtures/ok/spec/053return.solc deleted file mode 100644 index 2ea7c8b6..00000000 --- a/crates/parser/tests/fixtures/ok/spec/053return.solc +++ /dev/null @@ -1,36 +0,0 @@ -data Bool = False | True; -data W = W(word); - - -/* For experiments, special handling when emitting code */ -function ereturn(x:a) -> b { let res: b; return res; } - -/* simulate match expression - x = match { | False => return 77; | True => W(22) } -*/ -function elimBool1(b:Bool) -> word { - let x : W; - x = W(1); - match b { - // this works - | False => x = ereturn(77); - // what about "return(return 77)"? - // this does not work (monomorphisation fails) - // | False => x = ereturn(ereturn(77)); - - | True => x = W(22); - } - - match x { - | W(y) => return y; - } - -} - - -contract ExpReturn { - function main() -> word { - return elimBool1(False); - // return elimBool1(True); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/06comp.solc b/crates/parser/tests/fixtures/ok/spec/06comp.solc deleted file mode 100644 index a74b3728..00000000 --- a/crates/parser/tests/fixtures/ok/spec/06comp.solc +++ /dev/null @@ -1,21 +0,0 @@ -contract Compose { - function compose(f,g) { - return lam (x) { - return f(g(x)); - } ; - } - - function id(x) { return x; } - - function idid() { return compose(id,id); } - - function foo() { - let f = idid(); - return f(42); - } - - function main() { - let f = compose(id,id); - return f(42); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/09not.solc b/crates/parser/tests/fixtures/ok/spec/09not.solc deleted file mode 100644 index 053e9a6a..00000000 --- a/crates/parser/tests/fixtures/ok/spec/09not.solc +++ /dev/null @@ -1,21 +0,0 @@ -contract Not { - data Bool = False | True; - - function main() { - return fromBool(bnot(False)); - } - - function fromBool(b) { - match(b) { - | False => return 0; - | True => return 1; - } - } - - function bnot(b) { - match b { - | False => return True; - | True => return False; - } - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/101struct1Field.solc b/crates/parser/tests/fixtures/ok/spec/101struct1Field.solc deleted file mode 100644 index 9e634bb4..00000000 --- a/crates/parser/tests/fixtures/ok/spec/101struct1Field.solc +++ /dev/null @@ -1,254 +0,0 @@ - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x;} -} - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - - -function mload_(x:word) -> word { - let res: word; - assembly { - res := mload(x) - } - return res; - } - -function mstore_(a:word, v:word) { - assembly { mstore(a,v) } -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(mload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return mstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field, Proxy(offset)); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z,p) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -// This is *a lot* of pragmas... -// pragma no-coverage-condition StructField, LValueMemberAccess, RValueMemberAccess; -// pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; -// pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(ptr); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , fieldType:MemoryType - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(ptr); - } -} - -////// Testing - -// struct S { fld1:word; } -data S = S(word); -data fld1_sel = fld1_sel; -// data y_sel = y_sel; -// data z_sel = z_sel; - -instance StructField(S, x_sel):StructField(word, ()) {} -// instance StructField(S, y_sel):StructField(uint, word) {} -// BUG: This next one should really be the following, but that breaks weirdly: -// (I get a patterson condition violation on an invoke instance for g) -instance StructField(S, z_sel):StructField(word, (word,uint)) {} -// So instead I use: -// instance StructField(S, z_sel):StructField(word, word) {} - - -function f() { - let x:memory(word); - let y:memory(word); - // x = y - Assign.assign(ref(x), y); - /* - * Idea in the above: to avoid overlapping instances, - * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), - * to be able to choose a disjoint assign instance. - * Of course this needs special treatment during code generation, - * on the other hand, stack assignments generally do... - * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. - */ -} - -function g() -> word { - let s:memory(S) = Typedef.abs(0x80); - - let offset0 : Proxy( () ) = Proxy; - // s.fld1 = y - let fld1_lval : memoryRef(word) - = LValueMemberAccess.memberAccess(MemberAccessProxy(s, fld1_sel, offset0)); - Assign.assign(fld1_lval, y); - // return s.fld1 - let r : word = 17; - r = RValueMemberAccess.memberAccess(MemberAccessProxy(s, fld1_sel, offset0) ); - return r; -} - -contract C { - function main() { - f(); - return g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/102uintField.solc b/crates/parser/tests/fixtures/ok/spec/102uintField.solc deleted file mode 100644 index 89a01289..00000000 --- a/crates/parser/tests/fixtures/ok/spec/102uintField.solc +++ /dev/null @@ -1,261 +0,0 @@ - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - - -function mload_(x:word) -> word { - let res: word; - assembly { - res := mload(x) - } - return res; - } - -function mstore_(a:word, v:word) { - assembly { mstore(a,v) } -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(mload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return mstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field, Proxy(offset)); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z,p) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -// This is *a lot* of pragmas... -// pragma no-coverage-condition StructField, LValueMemberAccess, RValueMemberAccess; -// pragma no-patterson-condition LValueMemberAccess, RValueMemberAccess; -// pragma no-bounded-variable-condition LValueMemberAccess, RValueMemberAccess; -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(ptr); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , fieldType:MemoryType - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(ptr):fieldType; - } -} - -////// Testing - -// struct S { fld1:uint; } -data S = S(uint); -data fld1_sel = fld1_sel; -// data y_sel = y_sel; -// data z_sel = z_sel; - -instance StructField(S, fld1_sel):StructField(uint, ()) {} -// instance StructField(S, y_sel):StructField(uint, uint) {} -// BUG: This next one should really be the following, but that breaks weirdly: -// (I get a patterson condition violation on an invoke instance for g) -// instance StructField(S, z_sel):StructField(word, (word,uint)) {} -// So instead I use: -// instance StructField(S, z_sel):StructField(word, word) {} - - -function f() { - let x:memory(word); - let y:memory(word); - // x = y - Assign.assign(ref(x), y); - /* - * Idea in the above: to avoid overlapping instances, - * we can desugar a simple identifier referring to a local variable on the lhs of an assignment to ref(x), - * to be able to choose a disjoint assign instance. - * Of course this needs special treatment during code generation, - * on the other hand, stack assignments generally do... - * Actually, even simpler might be just *not* to desugar assignments at all, if the lhs is just an identifier referring to a local variable and just directly take care of it when translating to core. - */ -} - -function g() -> word { - let s:memory(S) = Typedef.abs(0x80); - // let y:word = 42; - let z:uint = uint(42); - - let offset0 : Proxy( () ) = Proxy; - // s.fld1 = z - let fld1_lval : memoryRef(uint) - = LValueMemberAccess.memberAccess(MemberAccessProxy(s, fld1_sel, offset0)); - Assign.assign(fld1_lval, z); - // return s.fld1 - let r : uint = uint(17); - r = RValueMemberAccess.memberAccess(MemberAccessProxy(s, fld1_sel, offset0) ); - let r2 : word = Typedef.rep(r : uint); - return r2; -} - -contract C { - function main() { - f(); - return g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/103struct3Fields.solc b/crates/parser/tests/fixtures/ok/spec/103struct3Fields.solc deleted file mode 100644 index fd2b8af5..00000000 --- a/crates/parser/tests/fixtures/ok/spec/103struct3Fields.solc +++ /dev/null @@ -1,284 +0,0 @@ -// v4: Simplified Member AccessProxy (no Proxy(offset)) -// variables holding field MAPs - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - - -function mload_(x:word) -> word { - let res: word; - assembly { - res := mload(x) - } - return res; - } - -function mstore_(a:word, v:word) { - assembly { mstore(a,v) } -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(mload_(ptr)):uint; - } - function store(ptr:word, value:uint) -> () { - return mstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(ptr); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:MemorySize -=> instance a:MemorySize { - function size(x:Proxy(a)) -> word { - return MemorySize.size(Proxy(b)); - } -} -*/ - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , fieldType:MemoryType - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(ptr):fieldType; - } -} - -////// Testing - -// struct S { fld1:uint; fld2:word; fld3:word } -data S = S; // (uint, word, word); -data fld1_sel = fld1_sel; -data fld2_sel = fld2_sel; -data fld3_sel = fld3_sel; - -// form: -// instance StructField(S, f_sel):StructField(ftype, preceding)) {} -instance StructField(S, fld1_sel):StructField(uint, ()) {} -instance StructField(S, fld2_sel):StructField(word, uint) {} -instance StructField(S, fld3_sel):StructField(word, (uint, word)) {} - - -function g() -> word { - let s:memory(S) = Typedef.abs(0x80); - let fld1_map : MemberAccessProxy(memory(S), fld1_sel, ()) = MemberAccessProxy(s, fld1_sel); - let fld2_map : MemberAccessProxy(memory(S), fld2_sel, uint) = MemberAccessProxy(s, fld2_sel); - let fld3_map = MemberAccessProxy(s, fld3_sel) - : MemberAccessProxy(memory(S), fld3_sel, (uint,word)); - // let y:word = 13; - let z:uint = uint(13); - - // s.fld1 = z - - let fld1_lval : memoryRef(uint) - = LValueMemberAccess.memberAccess(fld1_map ); - Assign.assign(fld1_lval, z); - - // s.fld2 = 14 - let fld2_lval // : memoryRef(word) - = LValueMemberAccess.memberAccess(fld2_map); - Assign.assign(fld2_lval, 14); - - // s.fld3 = 15 - let fld3_lval // : memoryRef(word) - = LValueMemberAccess.memberAccess(fld3_map); - Assign.assign(fld3_lval, 15); - - // let f1 = S.fld1 - let f1 : uint; - f1 = RValueMemberAccess.memberAccess(fld1_map); - - let f2 : word; - f2 = RValueMemberAccess.memberAccess(fld2_map); - - let f3 : word; - f3 = RValueMemberAccess.memberAccess(fld3_map); - - let f12 = add(Typedef.rep(f1) : word, f2); - let f123 = add(f12, f3); - - return f123; - -} - -contract C { - function main() { - return g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/105nestedStruct.solc b/crates/parser/tests/fixtures/ok/spec/105nestedStruct.solc deleted file mode 100644 index 6e36ed10..00000000 --- a/crates/parser/tests/fixtures/ok/spec/105nestedStruct.solc +++ /dev/null @@ -1,343 +0,0 @@ -// v5: nested struct -// variables holding field MAPs - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data memory(a) = memory(word); -data memoryRef(a) = memoryRef(word); -data Proxy(a) = Proxy; - -instance memory(a):Typedef(word) { - function rep(x:memory(a)) -> word { - match x { - | memory(y) => return y; - } - } - function abs(x:word) -> memory(a) { - return memory(x); - } -} -instance memoryRef(a):Typedef(word) { - function rep(x:memoryRef(a)) -> word { - match x { - | memoryRef(y) => return y; - } - } - function abs(x:word) -> memoryRef(a) { - return memoryRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -class self:MemoryType { - function load(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:MemorySize { - function size(x:Proxy(self)) -> word; -} - - -function mload_(x:word) -> word { - let res: word; - assembly { - res := mload(x) - } - return res; - } - -function mstore_(a:word, v:word) { - assembly { mstore(a,v) } -} - -instance word:MemoryType { - function load(ptr:word) -> word { - let r:word; - assembly { - r := mload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - mstore(ptr, value) - } - } -} - -instance uint:MemoryType { - function load(ptr:word) -> uint { - return Typedef.abs(mload_(ptr)); - } - function store(ptr:word, value:uint) -> () { - return mstore_(ptr, Typedef.rep(value)); - } -} - -forall a . instance memory(a):MemoryType { - function load(ptr:word) -> memory(a) { - return Typedef.abs(mload_(ptr)); - } - function store(ptr:word, value:memory(a)) -> () { - return mstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : MemoryType => instance memoryRef(a):Assign(a) { - function assign(l:memoryRef(a), y:a) { - MemoryType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):LValueMemberAccess(memoryRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> memoryRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return memoryRef(ptr); - } -} - -instance ():MemorySize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:MemorySize { - function size(x:Proxy(word)) -> word { - return 32; - } -} - -instance uint:MemorySize { - function size(x:Proxy(uint)) -> word { - return 32; - } -} - -forall a -. instance memory(a):MemorySize { - function size(x:Proxy(memory(a))) -> word { - return 32; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:MemorySize -=> instance a:MemorySize { - function size(x:Proxy(a)) -> word { - return MemorySize.size(Proxy(b)); - } -} -*/ - -forall a b . a:MemorySize, b:MemorySize => instance (a,b):MemorySize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = MemorySize.size(Proxy:Proxy(a)); - let b_sz:word = MemorySize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , fieldType:MemoryType - , offsetType:MemorySize - => instance MemberAccessProxy(memory(structType), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(memory(structType), fieldSelector, offsetType)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = MemorySize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return MemoryType.load(ptr):fieldType; - } -} - -////// Testing - -// struct S { fld1:uint; fld2:word; fld3:word } -data S = S; // (uint, word, word); - -// struct W { flds : memory(W) } -data W = W; - -data fld1_sel = fld1_sel; -data fld2_sel = fld2_sel; -data fld3_sel = fld3_sel; - -data flds_sel = flds_sel; - -// form: -// instance StructField(S, f_sel):StructField(ftype, preceding)) {} -instance StructField(S, fld1_sel):StructField(uint, ()) {} -instance StructField(S, fld2_sel):StructField(word, uint) {} -instance StructField(S, fld3_sel):StructField(word, (uint, word)) {} - -instance StructField(W, flds_sel):StructField(memory(S), ()) {} - -function makeS() -> memory(S) { - let s:memory(S) = Typedef.abs(0x80); - let fld1_map : MemberAccessProxy(memory(S), fld1_sel, ()) = MemberAccessProxy(s, fld1_sel); - let fld2_map : MemberAccessProxy(memory(S), fld2_sel, uint) = MemberAccessProxy(s, fld2_sel); - let fld3_map = MemberAccessProxy(s, fld3_sel) - : MemberAccessProxy(memory(S), fld3_sel, (uint,word)); - // let y:word = 13; - let z:uint = uint(13); - - // s.fld1 = z - - let fld1_lval : memoryRef(uint) - = LValueMemberAccess.memberAccess(fld1_map ); - Assign.assign(fld1_lval, z); - - // s.fld2 = 14 - let fld2_lval // : memoryRef(word) - = LValueMemberAccess.memberAccess(fld2_map); - Assign.assign(fld2_lval, 14); - - // s.fld3 = 15 - let fld3_lval // : memoryRef(word) - = LValueMemberAccess.memberAccess(fld3_map); - Assign.assign(fld3_lval, 15); - return s; -} - -function readS(s:memory(S)) -> word { - let s:memory(S) = Typedef.abs(0x80); - let fld1_map : MemberAccessProxy(memory(S), fld1_sel, ()) = MemberAccessProxy(s, fld1_sel); - let fld2_map : MemberAccessProxy(memory(S), fld2_sel, uint) = MemberAccessProxy(s, fld2_sel); - let fld3_map = MemberAccessProxy(s, fld3_sel) - : MemberAccessProxy(memory(S), fld3_sel, (uint,word)); - - // let f1 = s.fld1 - let f1 : uint; - f1 = RValueMemberAccess.memberAccess(fld1_map); - - // let f2 = s.fld2 - let f2 : word; - f2 = RValueMemberAccess.memberAccess(fld2_map); - - let f3 : word; - f3 = RValueMemberAccess.memberAccess(fld3_map); - - let f12 = add(Typedef.rep(f1) : word, f2); - let f123 = add(f12, f3); - - return f123; -} - -function rwS() -> word { - let s:memory(S) = makeS(); - return readS(s); - -} - - -function makeW(s:memory(S)) -> memory(W) { - let w:memory(W) = Typedef.abs(0xe0); - let flds_map : MemberAccessProxy(memory(W), flds_sel, ()) = MemberAccessProxy(w, flds_sel); - - // w.flds = s - let flds_lval : memoryRef(memory(S)) - = LValueMemberAccess.memberAccess(flds_map ); - Assign.assign(flds_lval, s); - - return w; -} - -function readW(w:memory(W)) -> memory(S) { - let flds_map : MemberAccessProxy(memory(W), flds_sel, ()) = MemberAccessProxy(w, flds_sel); - return RValueMemberAccess.memberAccess(flds_map); -} - -contract C { - function main() { - let s:memory(S) = makeS(); - let w:memory(W) = makeW(s); - let s2:memory(S) = readW(w); - return readS(s2); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/10negBool.solc b/crates/parser/tests/fixtures/ok/spec/10negBool.solc deleted file mode 100644 index 3def07db..00000000 --- a/crates/parser/tests/fixtures/ok/spec/10negBool.solc +++ /dev/null @@ -1,29 +0,0 @@ - -forall a . class a : Neg { - function neg(x:a) -> a; -} - -data B = F | T; - - -instance B : Neg { - function neg (x : B) -> B { - match x { - | F => return T; - | T => return F; - } - } -} - - -contract NegBool { - - function fromB(b) { - match b { - | F => return 0; - | T => return 1; - } - } - - function main() { return fromB(Neg.neg(F)); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/111storageStruct.solc b/crates/parser/tests/fixtures/ok/spec/111storageStruct.solc deleted file mode 100644 index f3a85f36..00000000 --- a/crates/parser/tests/fixtures/ok/spec/111storageStruct.solc +++ /dev/null @@ -1,288 +0,0 @@ -// v4: Simplified Member AccessProxy (no Proxy(offset)) -// variables holding field MAPs - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -/////// Construction -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data storage(a) = storage(word); -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -instance storage(a):Typedef(word) { - function rep(x:storage(a)) -> word { - match x { - | storage(y) => return y; - } - } - function abs(x:word) -> storage(a) { - return storage(x); - } -} -instance storageRef(a):Typedef(word) { - function rep(x:storageRef(a)) -> word { - match x { - | storageRef(y) => return y; - } - } - function abs(x:word) -> storageRef(a) { - return storageRef(x); - } -} - -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -/* -data ref(a) = ref(a); - -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} -*/ - -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -class self:StorageSize { - function size(x:Proxy(self)) -> word; -} - - -function sload_(x:word) -> word { - let res: word; - assembly { - res := sload(x) - } - return res; - } - -function sstore_(a:word, v:word) { - assembly { sstore(a,v) } -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -instance uint:StorageType { - function sload(ptr:word) -> uint { - return Typedef.abs(sload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : StorageType => instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) { - StorageType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return storageRef(ptr); - } -} - -instance ():StorageSize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:StorageSize { - function size(x:Proxy(word)) -> word { - return 1; - } -} - -instance uint:StorageSize { - function size(x:Proxy(uint)) -> word { - return 1; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:StorageSize -=> instance a:StorageSize { - function size(x:Proxy(a)) -> word { - return StorageSize.size(Proxy(b)); - } -} -*/ - -forall a b . a:StorageSize, b:StorageSize => instance (a,b):StorageSize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = StorageSize.size(Proxy:Proxy(a)); - let b_sz:word = StorageSize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> fieldType { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return StorageType.sload(ptr):fieldType; - } -} - -// helpers - -////// Testing - -// struct S { fld1:uint; fld2:word; fld3:word } -data S = S; // (uint, word, word); -data fld1_sel = fld1_sel; -data fld2_sel = fld2_sel; -data fld3_sel = fld3_sel; - -// form: -// instance StructField(S, f_sel):StructField(ftype, preceding)) {} -instance StructField(S, fld1_sel):StructField(uint, ()) {} -instance StructField(S, fld2_sel):StructField(word, uint) {} -instance StructField(S, fld3_sel):StructField(word, (uint, word)) {} - - -function g() -> word { - let s:storage(S) = Typedef.abs(0x80); - let fld1_map : MemberAccessProxy(storage(S), fld1_sel, ()) = MemberAccessProxy(s, fld1_sel); - let fld2_map : MemberAccessProxy(storage(S), fld2_sel, uint) = MemberAccessProxy(s, fld2_sel); - let fld3_map = MemberAccessProxy(s, fld3_sel) - : MemberAccessProxy(storage(S), fld3_sel, (uint,word)); - // let y:word = 13; - let z:uint = uint(13); - - // s.fld1 = z - - let fld1_lval : storageRef(uint) - = LValueMemberAccess.memberAccess(fld1_map ); - Assign.assign(fld1_lval, z); - - // s.fld2 = 14 - let fld2_lval // : storageRef(word) - = LValueMemberAccess.memberAccess(fld2_map); - Assign.assign(fld2_lval, 14); - - // s.fld3 = 15 - let fld3_lval // : storageRef(word) - = LValueMemberAccess.memberAccess(fld3_map); - Assign.assign(fld3_lval, 15); - - // let f1 = S.fld1 - let f1 : uint; - f1 = RValueMemberAccess.memberAccess(fld1_map); - - let f2 : word; - f2 = RValueMemberAccess.memberAccess(fld2_map); - - let f3 : word; - f3 = RValueMemberAccess.memberAccess(fld3_map); - - let f12 = add(Typedef.rep(f1) : word, f2); - let f123 = add(f12, f3); - - return f123; - -} - -contract C { - function main() { - return g(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/112ContractStorage.solc b/crates/parser/tests/fixtures/ok/spec/112ContractStorage.solc deleted file mode 100644 index bc24c4a0..00000000 --- a/crates/parser/tests/fixtures/ok/spec/112ContractStorage.solc +++ /dev/null @@ -1,43 +0,0 @@ -import StorageLib; - -/* -// Translating contract: -contract Counter { - counter : word; - - function main() -> word { - counter = add(counter, 1); - return counter; - } -} -*/ - - - -// form: -// instance StructField(S, f_sel):StructField(ftype, preceding)) {} -data CounterCxt = CounterCxt; -data counter_sel = counter_sel; -instance StructField(ContractStorage(CounterCxt), counter_sel):StructField(word, ()) {} - -contract Counter { - // struct CounterCxt { counter:word } - - function main() -> word { - let cxt : ContractStorage(CounterCxt) = ContractStorage(CounterCxt); - let counter_map : MemberAccessProxy(ContractStorage(CounterCxt), counter_sel, ()) - = MemberAccessProxy(cxt, counter_sel); - - // let c1 = this.counter - let c1 : word; - c1 = RValueMemberAccess.memberAccess(counter_map); - - // this.counter = c1 + 7 - let counter_lval // : storageRef(word) - = LValueMemberAccess.memberAccess(counter_map); - - Assign.assign(counter_lval, add(c1, 7)); - - return RValueMemberAccess.memberAccess(counter_map); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/113counter.solc b/crates/parser/tests/fixtures/ok/spec/113counter.solc deleted file mode 100644 index e65f1cb3..00000000 --- a/crates/parser/tests/fixtures/ok/spec/113counter.solc +++ /dev/null @@ -1,23 +0,0 @@ -import StorageLib; - -/* -contract Counter { - counter : word; - - function main() -> word { - counter = add(counter, 1); - return counter; - } -} -*/ - -data counter_sel = counter_sel; -instance StructField(ContractStorage(()), counter_sel):StructField(word, ()) {} - -contract Counter { - function main () -> word { - let counter_map /*: MemberAccessProxy(ContractStorage(()), counter_sel, ()) */ = MemberAccessProxy(ContractStorage(()), counter_sel); - Assign.assign(LValueMemberAccess.memberAccess(MemberAccessProxy(ContractStorage(()), counter_sel)), add(rval(counter_map), 1)); - return rval(counter_map); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/114map.solc b/crates/parser/tests/fixtures/ok/spec/114map.solc deleted file mode 100644 index d7018eda..00000000 --- a/crates/parser/tests/fixtures/ok/spec/114map.solc +++ /dev/null @@ -1,61 +0,0 @@ -import IndexLib; - -/* -contract Map { - reserved : word; - owner : address = address(0x123456789abcdef); - balances : mapping(address,word) // FIXME: address type - - function mint(amount:word) { - balances[owner] = word; // simplified - } - - function main() -> word { - mint(1000); - return balances[owner]; - } -} -*/ - - -data MapCtx = MapCtx; -data owner_sel = owner_sel; -data balances_sel = balances_sel; -// field reserved:word -instance StructField(ContractStorage(MapCtx), owner_sel):StructField(address, (word)) {} -instance StructField(ContractStorage(MapCtx), balances_sel):StructField(mapping(address,word), (word, address)) {} - -contract Map { - - forall sel ftype offset. StructField(ContractStorage(MapCtx), sel):StructField(ftype, offset) - => function proxy_for(s:sel) -> MemberAccessProxy(ContractStorage(MapCtx), sel, offset) { - return MemberAccessProxy(ContractStorage(MapCtx), s); - } - - function mint(amount:word) { - let owner_prx = proxy_for(owner_sel); - let bal_prx = proxy_for(balances_sel); - let bal_ref : storageRef(mapping(address,word)) = LValueMemberAccess.memberAccess(bal_prx); - let owner_bal_prx = IndexAccessProxy(bal_ref, rval(owner_prx)); - let ref : storageRef(word)= LValueMemberAccess.memberAccess(owner_bal_prx); - - Assign.assign(LValueMemberAccess.memberAccess(owner_bal_prx), amount) ; - } - - function main () -> word { - let ctx = ContractStorage(MapCtx); - let owner_prx /*: MemberAccessProxy(ctx, owner_sel, ()) */ = MemberAccessProxy(ctx, owner_sel); - // owner = address(0x123456789abcdef); - Assign.assign(LValueMemberAccess.memberAccess(owner_prx), address(0x123456789abcdef)); - - mint(1000); - - // return balances[owner]; - - let bal_prx = proxy_for(balances_sel); - let bal_ref : storageRef(mapping(address,word)) = LValueMemberAccess.memberAccess(bal_prx); - let owner_bal_prx = IndexAccessProxy(bal_ref, rval(owner_prx)); - return RValueMemberAccess.memberAccess(owner_bal_prx) : word; - - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/11negPair.solc b/crates/parser/tests/fixtures/ok/spec/11negPair.solc deleted file mode 100644 index 5946b9f2..00000000 --- a/crates/parser/tests/fixtures/ok/spec/11negPair.solc +++ /dev/null @@ -1,53 +0,0 @@ - -forall a . class a : Neg { - function neg(x:a) -> a; -} - -data B = F | T; - -instance B : Neg { - function neg (x : B) -> B { - match x { - | F => return T; - | T => return F; - } - } -} - -function fst (p) { - match p { - | (x,y) => return x; - } -} - -function snd(p) { - match p { - | (x,y) => return y; - } -} - - -forall a b . a : Neg, b : Neg => instance (a,b):Neg { - function neg(p : (a,b)) -> (a,b) { - return (Neg.neg (fst(p)), Neg.neg(snd (p))); - } -} - -contract NegPair { - - function bnot(x) { - match x { - | T => return F; - | F => return T; - } -} - - function fromB(b) { - match b { - | F => return 0; - | T => return 1; - } -} - - function main() { return fromB(fst(Neg.neg((F,T)))); } -} diff --git a/crates/parser/tests/fixtures/ok/spec/120basicCounter.solc b/crates/parser/tests/fixtures/ok/spec/120basicCounter.solc deleted file mode 100644 index e447d3b5..00000000 --- a/crates/parser/tests/fixtures/ok/spec/120basicCounter.solc +++ /dev/null @@ -1,9 +0,0 @@ -import std; -contract Counter { - counter : word; - - function main() -> word { - counter = Num.add(counter, 42); - return counter; - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/121counter.solc b/crates/parser/tests/fixtures/ok/spec/121counter.solc deleted file mode 100644 index a2ed104f..00000000 --- a/crates/parser/tests/fixtures/ok/spec/121counter.solc +++ /dev/null @@ -1,11 +0,0 @@ -// test single contract field -import std; - -contract Counter { - counter : word; - - function main() -> word { - counter = Num.add(counter, 1); - return counter; - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/126nanoerc20.solc b/crates/parser/tests/fixtures/ok/spec/126nanoerc20.solc deleted file mode 100644 index f098b28c..00000000 --- a/crates/parser/tests/fixtures/ok/spec/126nanoerc20.solc +++ /dev/null @@ -1,80 +0,0 @@ -import std; - -function caller() -> address { - let res: word; - assembly { - res := caller() - } - return address(res); -} - -function myrevert( msg: (word, word) ) -> () { - match msg { - | (str, len) => - let str1 = str; let len1 = len; - assembly { mstore(0, str1) revert(0, len1) } - } -} - -function require(cond: bool, msg: (word, word) ) { - if( not(cond) ) { myrevert(msg); } -} - -function require1(cond: bool) { - require (cond, (0x72657175697265313a204641494c, 14) /* "require1: FAIL" */ ); -} - - -function nop() -> () { return ();} - -contract Uint { - reserved : word; - msg_sender : address; // mock msg.sender - owner : address; - decimals : uint256; - totalSupply : uint256; - balances : mapping(address,uint256); - - function mint(amount:uint256) { - balances[owner] = Num.add(balances[owner], amount); - totalSupply = Num.add(totalSupply, amount); - } - - // function transferFrom(address src, address dst, uint256 amt) public returns (bool) - function transferFrom(src:address, dst:address, amt:uint256) -> bool { - require1(ge(balances[src], amt)); - - /* - balances[src] = Num.sub(balances[src], amt); - balances[dst] = Num.add(balances[dst], amt):uint256; - */ - withdraw(src, amt); //workaround typechecker quirk - deposit(dst, amt); - return true; - } - - - function withdraw(src:address, amt:uint256) { - balances[src] = Num.sub(balances[src], amt):uint256; - } - - function deposit(dst:address, amt:uint256) { - balances[dst] = Num.add(balances[dst], amt):uint256; - } - - function init() { - owner = address(0x123456789abcdef); - msg_sender = caller(); - decimals = uint256(18); - } - - function main() -> uint256 { - init(); - mint(uint256(1000)); - let src : address = owner; - transferFrom(owner, msg_sender, uint256(42)); - - return balances[msg_sender] : uint256; - } -} - diff --git a/crates/parser/tests/fixtures/ok/spec/127microerc20.solc b/crates/parser/tests/fixtures/ok/spec/127microerc20.solc deleted file mode 100644 index 85cf0454..00000000 --- a/crates/parser/tests/fixtures/ok/spec/127microerc20.solc +++ /dev/null @@ -1,103 +0,0 @@ -import std; - -function caller() -> address { - let res: word; - assembly { - res := caller() - } - return address(res); -} - -function require1fail() { - let res: word; - assembly { - mstore(0x0, 0x72657175697265313a204641494c) // "require1: FAIL" - revert(0,32) - } - return (); // for the typechecker -} - -function require1(cond: bool) { - match cond { - | false => return require1fail(); - | true => return (); - } -} - -function nop() -> () { return ();} - -contract Mini { - reserved : word; - msg_sender : address; // mock msg.sender - owner : address; - decimals : uint256; - totalSupply : uint256; - balances : mapping(address,uint256); - allowance : mapping(address, mapping(address, uint256)); - - function mint(amount:uint256) -> () { - balances[owner] = Num.add(balances[owner], amount); - totalSupply = Num.add(totalSupply, amount); - } - -/* // original: - function transferFrom(address src, address dst, uint256 amt) public returns (bool) { - require(balanceOf[src] >= amt, "token/insufficient-balance"); - if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { - require(allowance[src][msg.sender] >= amt, "token/insufficient-allowance"); - allowance[src][msg.sender] -= amt; - } - - balanceOf[src] -= amt; - balanceOf[dst] += amt; - emit Transfer(src, dst, amt); - return true; - } -*/ - -// function transferFrom(src:address, dst:address, amt:uint256) -> bool { - function transferFrom(src, dst, amt) -> bool { - require1(ge(balances[src], amt)); - - match (Eq.eq(src, msg_sender)) { - | true => match ne(allowance[src][msg_sender], Num.maxVal():uint256) { - | true => require1(false); - | false => (); - } - | false => (); - } - -/* - if ((src != msg_sender) && (allowance [src][msg_sender] != (Num.maxVal():uint256)) ) { - require1(allowance[src][msg.sender] >= amt); - } -*/ - balances[src] = Num.sub(balances[src], amt); - balances[dst] = Num.add(balances[dst], amt):uint256; - return true; - } - -/* - function approve(address usr, uint256 amt) public returns (bool) { - allowance[msg.sender][usr] = amt; - emit Approval(msg.sender, usr, amt); - return true; - } -*/ - - - function init() -> () { - owner = address(0x123456789abcdef); - msg_sender = caller(); - decimals = uint256(18); - } - - function main() -> uint256 { - init(); - mint(uint256(1000)); - allowance[owner][msg_sender] = uint256(10000); - transferFrom(owner, msg_sender, uint256(42)); - - return balances[msg_sender] : uint256; - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/128minierc20.solc b/crates/parser/tests/fixtures/ok/spec/128minierc20.solc deleted file mode 100644 index 9e17dbd9..00000000 --- a/crates/parser/tests/fixtures/ok/spec/128minierc20.solc +++ /dev/null @@ -1,94 +0,0 @@ -import std; - -function caller() -> address { - let res: word; - assembly { - res := caller() - } - return address(res); -} - -function myrevert(msg: word) -> () { - assembly { mstore(0, msg) revert(0, 32) } -} - -function require(cond: bool, msg: word ) { - if( !cond ) { myrevert(msg); } -} - -contract MiniERC20 { - reserved : word; // forge idiosyncrasies - owner : address; - decimals : uint256; - totalSupply : uint256; - balances : mapping(address,uint256); - allowance : mapping(address, mapping(address, uint256)); - - function mint(amount:uint256) -> () { - balances[owner] = Num.add(balances[owner], amount); - totalSupply = Num.add(totalSupply, amount); - } - -/* // original: - function transferFrom(address src, address dst, uint256 amt) public returns (bool) { - require(balanceOf[src] >= amt, "token/insufficient-balance"); - if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { - require(allowance[src][msg.sender] >= amt, "token/insufficient-allowance"); - allowance[src][msg.sender] -= amt; - } - - balanceOf[src] -= amt; - balanceOf[dst] += amt; - emit Transfer(src, dst, amt); - return true; - } -*/ - - function transferFrom(src:address, dst:address, amt:uint256) -> bool { - let msg_sender = caller(); - require( balances[src] >= amt /* "token/insufficient-balance" */ - , 0x746f6b656e2f696e73756666696369656e742d62616c616e6365 - ); - - if (src != msg_sender && allowance[src][msg_sender] != (Num.maxVal():uint256)) { - require( allowance[src][msg_sender] >= amt /* "token/insufficient-allowance" */ - , 0x746f6b656e2f696e73756666696369656e742d616c6c6f77616e6365 - ); - allowance[src][msg_sender] -= amt; - } - balances[src] = balances[src] - amt; - balances[dst] = balances[dst] + amt; - return true; - } - -/* - function approve(address usr, uint256 amt) public returns (bool) { - allowance[msg.sender][usr] = amt; - emit Approval(msg.sender, usr, amt); - return true; - } -*/ - - function approve(usr: address, amt: uint256) -> bool { - let msg_sender = caller(); - allowance[msg_sender][usr] = amt; - // emit Approval(msg.sender, usr, amt); - return true; - - } - - function init() -> () { - owner = address(0x123456789abcdef); - decimals = uint256(18); // Num.fromWord(18) fails, which may be a problem - } - - function main() -> uint256 { - let msg_sender = caller(); - init(); - mint(uint256(1000)); - allowance[owner][msg_sender] = uint256(1000); - transferFrom(owner, msg_sender, uint256(42)); - - return allowance[owner][msg_sender]; - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/131constructor.solc b/crates/parser/tests/fixtures/ok/spec/131constructor.solc deleted file mode 100644 index 584847e1..00000000 --- a/crates/parser/tests/fixtures/ok/spec/131constructor.solc +++ /dev/null @@ -1,27 +0,0 @@ -// test constructor - -contract Counter { - - function setCounter(v: word) { - assembly { - sstore(0x00, v) - } - } - - function getCounter() -> word { - let res; - assembly { - res := sload(0x00) - } - return res; - } - - - constructor() { - setCounter(42); - } - - function main() -> word { - return getCounter(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/903badassign.solc b/crates/parser/tests/fixtures/ok/spec/903badassign.solc deleted file mode 100644 index 933261d7..00000000 --- a/crates/parser/tests/fixtures/ok/spec/903badassign.solc +++ /dev/null @@ -1,25 +0,0 @@ -contract Option { - data Option(a) = None | Some(a); - - function just(x) { return Some(x); } - - function maybe(n, o) { - match o { - | None => return n; - | Some(x) => return x; - } - } - - function join(mmx) { - let result = None; - match mmx { - | Some(Some(x)) => result = Some(x); - | None => result = None; - } - return result; - } - - function main() { - return maybe(0, join(Some(Some(42)))); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/939badfood.solc b/crates/parser/tests/fixtures/ok/spec/939badfood.solc deleted file mode 100644 index 431eb13a..00000000 --- a/crates/parser/tests/fixtures/ok/spec/939badfood.solc +++ /dev/null @@ -1,21 +0,0 @@ -forall a . class a: Enum { - function fromEnum(x : a) -> word; -} - -data Food = Curry | Beans | Other; - -instance Food : Enum { - function fromEnum(x : Food) -> word { - match x { - | Curry => return 1; - | Beans => return 2; - | Other => return 3; - } - } -} - -contract Food { - function main() { - return Enum.fromEnum(Beans); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/IndexLib.solc b/crates/parser/tests/fixtures/ok/spec/IndexLib.solc deleted file mode 100644 index cf6118ee..00000000 --- a/crates/parser/tests/fixtures/ok/spec/IndexLib.solc +++ /dev/null @@ -1,369 +0,0 @@ -import NumLib; - -/////// Construction -forall abs rep. -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance word:Typedef(word) { - function rep(x:word) -> word { return x; } - function abs(x:word) -> word { return x; } -} - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data address = address(word); - -instance address:Typedef(word) { - function rep(x:address) -> word { - match x { - | address(y) => return y; - } - } - function abs(x:word) -> address { - return address(x); - } -} - -instance address:Eq { - function eq(x : address , y : address) -> Bool { - return Eq.eq(Typedef.rep(x), Typedef.rep(y)); - } -} - -data storage(a) = storage(word); -data ContractStorage(cxt) = ContractStorage(cxt); - -data storageRef(a) = storageRef(word); - -data mapping(member, index) = mapping(word, Proxy(member), Proxy(index)); // storage by default -data mapRef(a) = mapRef(word); //ref to a map elem - -// data memoryRef(a) = memoryRef(word); - -forall a. -instance storage(a):Typedef(word) { - function rep(x:storage(a)) -> word { - match x { - | storage(y) => return y; - } - } - function abs(x:word) -> storage(a) { - return storage(x); - } -} - -forall a. -instance storageRef(a):Typedef(word) { - function rep(x:storageRef(a)) -> word { - match x { - | storageRef(y) => return y; - } - } - function abs(x:word) -> storageRef(a) { - return storageRef(x); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a. -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self. -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self. -class self:StorageSize { - function size(x:Proxy(self)) -> word; -} - - -function sload_(x:word) -> word { - let res: word; - assembly { - res := sload(x) - } - return res; - } - -function sstore_(a:word, v:word) { - assembly { sstore(a,v) } -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -instance uint:StorageType { - function sload(ptr:word) -> uint { - return Typedef.abs(sload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -instance address:StorageType { - function sload(ptr:word) -> address { - return Typedef.abs(sload_(ptr)):address; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:address) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : StorageType => instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) -> () { - StorageType.store(Typedef.rep(l), y); - } -} - -forall self fieldType offsetType. -class self:StructField(fieldType, offsetType) {} -data StructField(structType, fieldSelector) = StructField(structType); - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); - -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType. -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return storageRef(ptr); - } -} - -instance ():StorageSize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:StorageSize { - function size(x:Proxy(word)) -> word { - return 1; - } -} - -instance uint:StorageSize { - function size(x:Proxy(uint)) -> word { - return 1; - } -} - -instance address:StorageSize { - function size(x:Proxy(address)) -> word { - return 1; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:StorageSize -=> instance a:StorageSize { - function size(x:Proxy(a)) -> word { - return StorageSize.size(Proxy(b)); - } -} -*/ - -forall a b . a:StorageSize, b:StorageSize => instance (a,b):StorageSize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = StorageSize.size(Proxy:Proxy(a)); - let b_sz:word = StorageSize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -pragma no-patterson-condition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances -pragma no-coverage-condition MemberAccessProxy, LValueMemberAccess, RValueMemberAccess; - -// ------------------------------------------------------------------ -// Contract field access -// ------------------------------------------------------------------ - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = 0x100; // forge uses at least 1 storage slot - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - - assembly { - ptr := add(ptr, offsetSize) - } - return storageRef(ptr); // contract storage starts at 0 - } -} - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} - -/* -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(addW(ptr, offsetSize)):fieldType; - } -} -*/ -// ------------------------------------------------------------------ -// Indexed access -// ------------------------------------------------------------------ - -data mapping(index, member) = mapping(word); - -forall member index . instance mapping(index, member):Typedef(word) { - function rep(x:mapping(index, member)) -> word { - match x { - | mapping(y) => return y; - } - } - function abs(x:word) -> mapping(index,member) { - return mapping(x); - } -} - - -// cf https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays -forall index member . -instance mapping(index, member):StorageSize { - function size(x:Proxy(mapping(index, member))) -> word { - return 1; - } -} - -data IndexAccessProxy(map, index, member) = IndexAccessProxy(map, index); - -forall index member. index:Typedef(word) -=> instance IndexAccessProxy(storageRef(mapping(index,member)), index, member):LValueMemberAccess(storageRef(member)) { - function memberAccess(x:IndexAccessProxy(storageRef(mapping(index,member)), index, member)) -> storageRef(member) { - return storageRef(indexStorageSlot(x)); - } -} - -forall map index member . index:Typedef(word), member:StorageType, map:Typedef(word) -=> instance IndexAccessProxy(map, index, member):RValueMemberAccess(member) { - function memberAccess(x:IndexAccessProxy(map, index, member)) -> member { - let slot:word = indexStorageSlot(x); - return StorageType.sload(slot); - } -} - -forall index map member. map:Typedef(word), index:Typedef(word) => function indexStorageSlot(x:IndexAccessProxy(map, index, member)) -> word -//function indexStorageSlot(x) -{ - match x { - | IndexAccessProxy(map, i) => - let mapptr:word = Typedef.rep(map); - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} - -/* -forall index map member. map:Typedef(word), index:Typedef(word) -=> function indexedSlot(mapref : storageRef(mapping(index, member)), i: index) -> word -{ - match mapref { - | storageRef(mapptr) => - let rawidx:word = Typedef.rep(i); - let loc:word = hash2(mapptr, rawidx); - return loc; - } -} -*/ - -forall a b. a:RValueMemberAccess(b) => -function rval(x:a) -> b { - return RValueMemberAccess.memberAccess(x); -} diff --git a/crates/parser/tests/fixtures/ok/spec/SimpleField.solc b/crates/parser/tests/fixtures/ok/spec/SimpleField.solc deleted file mode 100644 index e22e4c9e..00000000 --- a/crates/parser/tests/fixtures/ok/spec/SimpleField.solc +++ /dev/null @@ -1,13 +0,0 @@ -import std; - -contract Simple { - myval : word ; - - function getVal () -> word { - return myval ; - } - - function main () -> word { - return getVal(); - } -} diff --git a/crates/parser/tests/fixtures/ok/spec/StorageLib.solc b/crates/parser/tests/fixtures/ok/spec/StorageLib.solc deleted file mode 100644 index 9889462d..00000000 --- a/crates/parser/tests/fixtures/ok/spec/StorageLib.solc +++ /dev/null @@ -1,253 +0,0 @@ -// v4: Simplified Member AccessProxy (no Proxy(offset)) -// variables holding field MAPs - -function add(x : word, y : word) { - let res: word; - assembly { - res := add(x, y) - } - return res; -} - -/////// Construction -forall abs rep. -class abs:Typedef(rep) { - function rep(x:abs) -> rep; - function abs(x:rep) -> abs; -} - - -data uint = uint(word); - -// this does not work :( -/* -forall a -. default instance a:Typedef(a) { - function rep(x:a) -> word { return a; } - function abs(x:a) -> word { return a;} -} -*/ - -instance uint:Typedef(word) { - function rep(x:uint) -> word { - match x { - | uint(y) => return y; - } - } - function abs(x:word) -> uint { - return uint(x); - } -} - -data storage(a) = storage(word); -data ContractStorage(cxt) = ContractStorage(cxt); - -data storageRef(a) = storageRef(word); -data Proxy(a) = Proxy; - -forall a. -instance storage(a):Typedef(word) { - function rep(x:storage(a)) -> word { - match x { - | storage(y) => return y; - } - } - function abs(x:word) -> storage(a) { - return storage(x); - } -} - -forall a. -instance storageRef(a):Typedef(word) { - function rep(x:storageRef(a)) -> word { - match x { - | storageRef(y) => return y; - } - } - function abs(x:word) -> storageRef(a) { - return storageRef(x); - } -} - -forall lhs rhs. -class lhs:Assign(rhs) { - function assign(l:lhs, r:rhs) -> (); -} - -data ref(a) = ref(a); - -forall a. -instance ref(a):Assign(a) { - function assign(l:ref(a), r:a) -> () { - // builtin "stack store" - return (); - } -} - -forall self. -class self:StorageType { - function sload(ptr:word) -> self; - function store(ptr:word, value:self) -> (); -} - -forall self. -class self:StorageSize { - function size(x:Proxy(self)) -> word; -} - - -function sload_(x:word) -> word { - let res: word; - assembly { - res := sload(x) - } - return res; - } - -function sstore_(a:word, v:word) { - assembly { sstore(a,v) } -} - -instance word:StorageType { - function sload(ptr:word) -> word { - let r:word; - assembly { - r := sload(ptr); - } - return r; - } - function store(ptr:word, value:word) -> () { - assembly { - sstore(ptr, value) - } - } -} - -instance uint:StorageType { - function sload(ptr:word) -> uint { - return Typedef.abs(sload_(ptr)):uint; // type annotation needed due to a typechecker bug - } - function store(ptr:word, value:uint) -> () { - return sstore_(ptr, Typedef.rep(value)); - } -} - -forall a . a : StorageType => instance storageRef(a):Assign(a) { - function assign(l:storageRef(a), y:a) -> () { - StorageType.store(Typedef.rep(l), y); - } -} - - - -data MemberAccessProxy(a, field, offset) = MemberAccessProxy(a, field); -forall a field offset . -function memberAccessD1(x:MemberAccessProxy(a, field, offset)) -> a { - match x { - | MemberAccessProxy(y,z) => return y; - } -} - -forall self memberRefType . -class self:LValueMemberAccess(memberRefType) { - function memberAccess(x:self) -> memberRefType; -} - -forall self memberValueType . -class self:RValueMemberAccess(memberValueType) { - function memberAccess(x:self) -> memberValueType; -} - -forall self fieldType offsetType . -class self:StructField(fieldType, offsetType) {} - -data StructField(structType, fieldSelector) = StructField(structType); - -forall structType fieldSelector fieldType offsetType - . StructField(structType, fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(storage(structType), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(storage(structType), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = Typedef.rep(memberAccessD1(x)); - let size:word = StorageSize.size(Proxy:Proxy(offsetType)); - assembly { - ptr := add(ptr, size) - } - return storageRef(ptr); - } -} - -instance ():StorageSize { - function size(x:Proxy(())) -> word { - return 0; - } -} - -instance word:StorageSize { - function size(x:Proxy(word)) -> word { - return 1; - } -} - -instance uint:StorageSize { - function size(x:Proxy(uint)) -> word { - return 1; - } -} - - -/* -// fails Patterson cond -forall a b . a:Typedef(b), b:StorageSize -=> instance a:StorageSize { - function size(x:Proxy(a)) -> word { - return StorageSize.size(Proxy(b)); - } -} -*/ - -forall a b . a:StorageSize, b:StorageSize => instance (a,b):StorageSize { - function size(x:Proxy((a,b))) -> word { - let a_sz:word = StorageSize.size(Proxy:Proxy(a)); - let b_sz:word = StorageSize.size(Proxy:Proxy(b)); - assembly { - a_sz := add(a_sz, b_sz) - } - return a_sz; - } -} - -pragma no-patterson-condition RValueMemberAccess; // this is due to ContractStorage(cxt); probably not needed once we have local instances -pragma no-coverage-condition LValueMemberAccess, RValueMemberAccess; - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):LValueMemberAccess(storageRef(fieldType)) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> storageRef(fieldType) { - let ptr:word = 0x100; // forge uses at least 1 storage slot - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - - assembly { - ptr := add(ptr, offsetSize) - } - return storageRef(ptr); // contract storage starts at 0 - } -} - -forall cxt fieldSelector fieldType offsetType - . StructField(ContractStorage(cxt), fieldSelector):StructField(fieldType, offsetType) - , fieldType:StorageType - , offsetType:StorageSize - => instance MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType):RValueMemberAccess(fieldType) { - function memberAccess(x:MemberAccessProxy(ContractStorage(cxt), fieldSelector, offsetType)) -> fieldType { - let ptr:word = 0x100; - let offsetSize:word = StorageSize.size(Proxy:Proxy(offsetType)); - return StorageType.sload(add(ptr, offsetSize)):fieldType; - } -} - -forall a b. a:RValueMemberAccess(b) => -function rval(x:a) -> b { - return RValueMemberAccess.memberAccess(x); -} diff --git a/crates/parser/tests/fixtures/ok/tuple_unit_sail.solc b/crates/parser/tests/fixtures/ok/tuple_unit_sail.solc new file mode 100644 index 00000000..421b6cab --- /dev/null +++ b/crates/parser/tests/fixtures/ok/tuple_unit_sail.solc @@ -0,0 +1,39 @@ +enum Pair { + Pair(A, B), +} + +function fst(p: (A, B)) returns (A) { + match (p) { + case (x, y) { + return x; + } + } +} + +function tupleValue() returns (word, word) { + return (1, 0); +} + +function unitValue() { + return (); +} + +function nestedTupleUnitPattern(p) { + match (p) { + case ((), (x, y)) { + return x; + } + } +} + +function groupedSinglePattern(p) { + match (p) { + case (y) { + return y; + } + } +} + +function pairData(x: word, y: word) returns (Pair) { + return Pair.Pair(x, y); +} diff --git a/crates/parser/tests/incremental_spans.rs b/crates/parser/tests/incremental_spans.rs new file mode 100644 index 00000000..149a9735 --- /dev/null +++ b/crates/parser/tests/incremental_spans.rs @@ -0,0 +1,716 @@ +//! Proves the anchor-relative span design keeps a def's HIR byte-shift +//! invariant: editing *above* a definition must not change its relative span +//! (the property that lets Salsa backdate the def's downstream queries), while +//! absolute resolution still tracks the edit. + +use std::sync::{Arc, Mutex}; + +use hir::{ + ast::{ + function::{ExprKind, FuncBody, FunctionMutability, FunctionVisibility}, + item::{ + AdtDef, ClassDef, ContractDef, ContractItem, ContractKind, FunctionDef, Item, TypeAlias, + }, + ty::{FunctionTypeVisibility, TypeRefKind, TypeRefShapeKind}, + }, + input::SourceFile, + span::Spanned, +}; +use salsa::Setter; +use solcore_parser::parse_file_to_hir; + +#[salsa::db] +#[derive(Clone)] +struct TestDb { + storage: salsa::Storage, + executed: Arc>>, +} + +impl Default for TestDb { + fn default() -> Self { + let executed = Arc::new(Mutex::new(Vec::new())); + Self { + storage: salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + }))), + executed, + } + } +} + +impl TestDb { + fn take_executed(&self) -> Vec { + std::mem::take(&mut *self.executed.lock().expect("execution log lock")) + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl solcore_parser::Db for TestDb {} + +#[salsa::tracked] +fn function_relative_span<'db>(db: &'db dyn hir::Db, function: FunctionDef<'db>) -> (u32, u32) { + let span = function.span(db); + (span.begin().as_u32(), span.end().as_u32()) +} + +#[salsa::tracked] +fn function_leading_comment_text<'db>( + db: &'db dyn hir::Db, + function: FunctionDef<'db>, +) -> Vec { + function + .leading_comments(db) + .iter() + .map(|comment| comment.text.clone()) + .collect() +} + +#[salsa::tracked] +fn nested_item_semantic_names<'db>( + db: &'db dyn hir::Db, + adt: AdtDef<'db>, + class: ClassDef<'db>, + contract: ContractDef<'db>, +) -> (String, String, String) { + let ctor = adt + .ctors(db) + .first() + .expect("ADT constructor") + .name + .atom() + .text(db) + .to_owned(); + let method = class + .methods(db) + .first() + .expect("class method") + .name + .atom() + .text(db) + .to_owned(); + let field = contract + .fields(db) + .first() + .expect("contract field") + .name() + .atom() + .text(db) + .to_owned(); + (ctor, method, field) +} + +#[salsa::tracked] +fn nested_item_comment_texts<'db>( + db: &'db dyn hir::Db, + adt: AdtDef<'db>, + class: ClassDef<'db>, + contract: ContractDef<'db>, +) -> (String, String, String) { + let ctor = adt.ctor_comments(db)[0][0].text.clone(); + let method = class.method_comments(db)[0][0].text.clone(); + let field = contract.field_comments(db)[0][0].text.clone(); + (ctor, method, field) +} + +#[salsa::tracked] +fn lambda_first_stmt_relative_span<'db>(db: &'db dyn hir::Db, body: FuncBody<'db>) -> (u32, u32) { + let stmt_id = body + .top_level_stmts(db) + .first() + .copied() + .expect("lambda body statement"); + let span = body.stmts(db).get(stmt_id).span(db); + (span.begin().as_u32(), span.end().as_u32()) +} + +fn first_function<'db>(db: &'db TestDb, file: SourceFile) -> FunctionDef<'db> { + parse_file_to_hir(db, file) + .module(db) + .items(db) + .iter() + .find_map(|item| match item { + Item::FunctionDef(def) => Some(*def), + _ => None, + }) + .expect("a top-level function") +} + +fn first_type_alias<'db>(db: &'db TestDb, file: SourceFile) -> TypeAlias<'db> { + parse_file_to_hir(db, file) + .module(db) + .items(db) + .iter() + .find_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .expect("a top-level type alias") +} + +fn first_lambda_body<'db>(db: &'db TestDb, file: SourceFile) -> FuncBody<'db> { + let function_body = first_function(db, file).body(db).expect("function body"); + function_body + .exprs(db) + .iter() + .find_map(|(_, expr)| match &expr.kind { + ExprKind::Lambda { body, .. } => Some(*body), + _ => None, + }) + .expect("lambda expression") +} + +fn nested_item_defs<'db>( + db: &'db TestDb, + file: SourceFile, +) -> (AdtDef<'db>, ClassDef<'db>, ContractDef<'db>) { + let module = parse_file_to_hir(db, file).module(db); + let adt = module + .items(db) + .iter() + .find_map(|item| match item { + Item::AdtDef(def) => Some(*def), + _ => None, + }) + .expect("a top-level ADT"); + let class = module + .items(db) + .iter() + .find_map(|item| match item { + Item::ClassDef(def) => Some(*def), + _ => None, + }) + .expect("a top-level class"); + let contract = module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(def) => Some(*def), + _ => None, + }) + .expect("a top-level contract"); + (adt, class, contract) +} + +fn first_contract_function<'db>( + db: &'db TestDb, + file: SourceFile, +) -> (ContractDef<'db>, FunctionDef<'db>) { + let contract = parse_file_to_hir(db, file) + .module(db) + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("a contract-like declaration"); + let function = contract + .items(db) + .iter() + .find_map(|item| match item { + ContractItem::FunctionDef(function) => Some(*function), + _ => None, + }) + .expect("a contract-like function"); + (contract, function) +} + +#[test] +fn top_level_error_item_has_recovery_span() { + let db = TestDb::default(); + let url = "memory:///recovery.solc".parse().expect("valid url"); + let src = "function first() {}\nunknown nonsense tokens\nfunction second() {}\n"; + let file = SourceFile::new(&db, url, Some(src.to_owned())); + + let module = parse_file_to_hir(&db, file).module(&db); + let error_item = module + .items(&db) + .iter() + .find(|item| matches!(item, Item::Error { .. })) + .expect("a recovered top-level error item"); + let absolute = error_item.span(&db).resolve_to_absolute(&db); + + assert_eq!(absolute.file(), file); + assert_eq!( + absolute.start().as_u32(), + src.find("unknown").expect("error text") as u32 + ); +} + +#[test] +fn shell_and_prototype_edits_update_tracked_hir_without_changing_def_identity() { + let mut db = TestDb::default(); + let file = SourceFile::new( + &db, + "memory:///shell-edit.solc".parse().expect("valid url"), + Some("interface Service { function run() external; }\n".to_owned()), + ); + + let before_identity = { + let (contract, function) = first_contract_function(&db, file); + assert_eq!(contract.kind(&db), ContractKind::Interface); + assert!(function.body(&db).is_none()); + ( + contract.def_id_value(&db).disambiguator(&db).as_u32(), + function.def_id_value(&db).disambiguator(&db).as_u32(), + ) + }; + + file.set_content(&mut db) + .to(Some("contract Service { function run() {} }\n".to_owned())); + + let (contract, function) = first_contract_function(&db, file); + assert_eq!(contract.kind(&db), ContractKind::Contract); + assert!(function.body(&db).is_some()); + assert_eq!( + ( + contract.def_id_value(&db).disambiguator(&db).as_u32(), + function.def_id_value(&db).disambiguator(&db).as_u32(), + ), + before_identity + ); +} + +#[test] +fn modifier_edits_update_the_signature_without_changing_def_identity() { + let mut db = TestDb::default(); + let file = SourceFile::new( + &db, + "memory:///modifier-edit.solc".parse().expect("valid url"), + Some("function run() public view {}\n".to_owned()), + ); + + let before_identity = { + let function = first_function(&db, file); + let sig = function.sig(&db); + assert_eq!(sig.visibility_kind(), Some(FunctionVisibility::Public)); + assert_eq!(sig.mutability_kind(), Some(FunctionMutability::View)); + ( + function.def_id_value(&db).disambiguator(&db).as_u32(), + function + .body(&db) + .expect("body") + .def_id(&db) + .disambiguator(&db) + .as_u32(), + ) + }; + + file.set_content(&mut db) + .to(Some("function run() external pure {}\n".to_owned())); + + let function = first_function(&db, file); + let sig = function.sig(&db); + assert_eq!(sig.visibility_kind(), Some(FunctionVisibility::External)); + assert_eq!(sig.mutability_kind(), Some(FunctionMutability::Pure)); + assert_eq!( + ( + function.def_id_value(&db).disambiguator(&db).as_u32(), + function + .body(&db) + .expect("body") + .def_id(&db) + .disambiguator(&db) + .as_u32(), + ), + before_identity + ); +} + +#[test] +fn function_type_qualifier_edits_update_shape_without_changing_alias_identity() { + let mut db = TestDb::default(); + let file = SourceFile::new( + &db, + "memory:///function-type-qualifier-edit.solc" + .parse() + .expect("valid url"), + Some("alias Callback = function(word) internal view returns (bool);\n".to_owned()), + ); + + let before_identity = { + let alias = first_type_alias(&db, file); + let ty = alias.ty(&db); + let TypeRefKind::Fn { + visibility: Some(visibility), + mutability: Some(mutability), + .. + } = ty.kind(&db) + else { + panic!("qualified function type"); + }; + assert_eq!(*visibility.atom(), FunctionTypeVisibility::Internal); + assert_eq!(*mutability.atom(), FunctionMutability::View); + let TypeRefShapeKind::Fn { + visibility, + mutability, + .. + } = ty.semantic_shape().kind(&db) + else { + panic!("function type shape"); + }; + assert_eq!(*visibility, Some(FunctionTypeVisibility::Internal)); + assert_eq!(*mutability, Some(FunctionMutability::View)); + alias.def_id_value(&db).disambiguator(&db).as_u32() + }; + + let after_src = "alias Callback = function(word) external payable returns (bool);\n"; + file.set_content(&mut db).to(Some(after_src.to_owned())); + + let alias = first_type_alias(&db, file); + assert_eq!( + alias.def_id_value(&db).disambiguator(&db).as_u32(), + before_identity + ); + let ty = alias.ty(&db); + let TypeRefKind::Fn { + span, + visibility: Some(visibility), + mutability: Some(mutability), + .. + } = ty.kind(&db) + else { + panic!("updated qualified function type"); + }; + assert_eq!(*visibility.atom(), FunctionTypeVisibility::External); + assert_eq!(*mutability.atom(), FunctionMutability::Payable); + let TypeRefShapeKind::Fn { + visibility: shape_visibility, + mutability: shape_mutability, + .. + } = ty.semantic_shape().kind(&db) + else { + panic!("updated function type shape"); + }; + assert_eq!(*shape_visibility, Some(FunctionTypeVisibility::External)); + assert_eq!(*shape_mutability, Some(FunctionMutability::Payable)); + + let type_text = "function(word) external payable returns (bool)"; + let expected_start = after_src.find(type_text).expect("updated type") as u32; + let absolute = span.resolve_to_absolute(&db); + assert_eq!(absolute.start().as_u32(), expected_start); + assert_eq!( + absolute.end().as_u32(), + expected_start + type_text.len() as u32 + ); + let visibility_absolute = visibility.span(&db).resolve_to_absolute(&db); + let visibility_start = after_src.find("external").expect("updated visibility") as u32; + assert_eq!(visibility_absolute.start().as_u32(), visibility_start); + assert_eq!( + visibility_absolute.end().as_u32(), + visibility_start + "external".len() as u32 + ); + let mutability_absolute = mutability.span(&db).resolve_to_absolute(&db); + let mutability_start = after_src.find("payable").expect("updated mutability") as u32; + assert_eq!(mutability_absolute.start().as_u32(), mutability_start); + assert_eq!( + mutability_absolute.end().as_u32(), + mutability_start + "payable".len() as u32 + ); +} + +#[test] +fn relative_span_query_backdates_after_edit_above_def() { + let mut db = TestDb::default(); + let url = "memory:///incr.solc".parse().expect("valid url"); + let src = "function id(x: word) returns (word) {\n return x;\n}\n"; + let file = SourceFile::new(&db, url, Some(src.to_owned())); + + // Baseline: execute the semantic-style query once, then drop all `'db` + // borrows so the input can be mutated. + let (before_fact, abs_start) = { + let func = first_function(&db, file); + let _ = db.take_executed(); + let fact = function_relative_span(&db, func); + let executed = db.take_executed(); + assert_eq!(relative_span_query_executions(&executed), 1); + + let rel = func.span(&db); + let abs = rel.resolve_to_absolute(&db); + // The function anchors on itself, so its relative span starts at 0, and + // with no leading text its absolute start is 0 too. + assert_eq!(rel.begin().as_u32(), 0); + assert_eq!(abs.start().as_u32(), 0); + (fact, abs.start().as_u32()) + }; + + // Insert a comment line *above* the function. + let prefix = "// a comment above\n"; + file.set_content(&mut db).to(Some(format!("{prefix}{src}"))); + + let (after_fact, abs) = { + let func = first_function(&db, file); + let _ = db.take_executed(); + let fact = function_relative_span(&db, func); + let executed = db.take_executed(); + assert_eq!(relative_span_query_executions(&executed), 0); + + let rel = func.span(&db); + (fact, rel.resolve_to_absolute(&db)) + }; + + // Relative fact is byte-identical and the tracked query did not re-execute. + assert_eq!(after_fact, before_fact); + // Absolute span shifted by exactly the inserted prefix length. + assert_eq!(abs.start().as_u32(), abs_start + prefix.len() as u32); +} + +#[test] +fn editing_leading_comment_invalidates_only_comment_consumers() { + let mut db = TestDb::default(); + let url = "memory:///comment-incr.solc".parse().expect("valid url"); + let file = SourceFile::new( + &db, + url, + Some("// one\nfunction id(x: word) returns (word) { return x; }\n".to_owned()), + ); + + let (before_identity, before_span) = { + let function = first_function(&db, file); + let def = function.def_id_value(&db); + let identity = ( + def.kind(&db), + def.name(&db), + def.disambiguator(&db).as_u32(), + ); + let span = function_relative_span(&db, function); + assert_eq!( + function_leading_comment_text(&db, function), + vec![" one".to_owned()] + ); + (identity, span) + }; + + file.set_content(&mut db).to(Some( + "// two\nfunction id(x: word) returns (word) { return x; }\n".to_owned(), + )); + + let function = first_function(&db, file); + let def = function.def_id_value(&db); + let after_identity = ( + def.kind(&db), + def.name(&db), + def.disambiguator(&db).as_u32(), + ); + let _ = db.take_executed(); + let after_span = function_relative_span(&db, function); + let after_comments = function_leading_comment_text(&db, function); + let executed = db.take_executed(); + + assert_eq!(after_identity, before_identity); + assert_eq!(after_span, before_span); + assert_eq!(after_comments, vec![" two".to_owned()]); + assert_eq!(relative_span_query_executions(&executed), 0); + assert_eq!(comment_query_executions(&executed), 1); +} + +#[test] +fn editing_nested_item_comments_preserves_semantic_fields() { + let mut db = TestDb::default(); + let url = "memory:///nested-comment-incr.solc" + .parse() + .expect("valid url"); + let before_src = "enum Choice { + // alpha + First +} +trait Documented { + // alpha + function describe(x: a) returns (word); +} +contract C { + // alpha + value: word; +} +"; + let file = SourceFile::new(&db, url, Some(before_src.to_owned())); + + let (before_semantics, before_comments) = { + let (adt, class, contract) = nested_item_defs(&db, file); + let _ = db.take_executed(); + let semantics = nested_item_semantic_names(&db, adt, class, contract); + let comments = nested_item_comment_texts(&db, adt, class, contract); + let executed = db.take_executed(); + assert_eq!(nested_semantic_query_executions(&executed), 1); + assert_eq!(nested_comment_query_executions(&executed), 1); + (semantics, comments) + }; + assert_eq!( + before_semantics, + ( + "First".to_owned(), + "describe".to_owned(), + "value".to_owned() + ) + ); + assert_eq!( + before_comments, + ( + " alpha".to_owned(), + " alpha".to_owned(), + " alpha".to_owned() + ) + ); + + // Keep the payload byte length unchanged so every nested declaration keeps + // the same owner-relative span. Only the parallel comment fields change. + file.set_content(&mut db).to(Some( + "enum Choice { + // bravo + First +} +trait Documented { + // bravo + function describe(x: a) returns (word); +} +contract C { + // bravo + value: word; +} +" + .to_owned(), + )); + + let (adt, class, contract) = nested_item_defs(&db, file); + let _ = db.take_executed(); + let after_semantics = nested_item_semantic_names(&db, adt, class, contract); + let after_comments = nested_item_comment_texts(&db, adt, class, contract); + let executed = db.take_executed(); + + assert_eq!(after_semantics, before_semantics); + assert_eq!( + after_comments, + ( + " bravo".to_owned(), + " bravo".to_owned(), + " bravo".to_owned() + ) + ); + assert_eq!(nested_semantic_query_executions(&executed), 0); + assert_eq!(nested_comment_query_executions(&executed), 1); +} + +#[test] +fn lambda_body_relative_span_backdates_after_cosmetic_signature_edit() { + let mut db = TestDb::default(); + let url = "memory:///lambda-incr.solc".parse().expect("valid url"); + let before_src = "function make(z: word) returns (word) { + let n = lam (x: word) returns (word) { + return x; + }; + return n(z); +} +"; + let file = SourceFile::new(&db, url, Some(before_src.to_owned())); + + let before_fact = { + let body = first_lambda_body(&db, file); + let _ = db.take_executed(); + let fact = lambda_first_stmt_relative_span(&db, body); + let executed = db.take_executed(); + assert_eq!(lambda_span_query_executions(&executed), 1); + fact + }; + + file.set_content(&mut db).to(Some( + "function make(z: word) returns (word) { + let n = lam ( + x /* same binder */: /* same parameter type */ word + ) returns (/* same return type */ word) { + return x; + }; + return n(z); +} +" + .to_owned(), + )); + + let after_cosmetic_fact = { + let body = first_lambda_body(&db, file); + let _ = db.take_executed(); + let fact = lambda_first_stmt_relative_span(&db, body); + let executed = db.take_executed(); + assert_eq!(lambda_span_query_executions(&executed), 0); + fact + }; + + assert_eq!(after_cosmetic_fact, before_fact); + + file.set_content(&mut db).to(Some( + "function make(z: word) returns (word) { + let n = lam (x: uint) returns (word) { + return x; + }; + return n(z); +} +" + .to_owned(), + )); + + let after_structural_fact = { + let body = first_lambda_body(&db, file); + let _ = db.take_executed(); + let fact = lambda_first_stmt_relative_span(&db, body); + let executed = db.take_executed(); + assert_eq!(lambda_span_query_executions(&executed), 1); + fact + }; + + assert_eq!(after_structural_fact, before_fact); +} + +fn relative_span_query_executions(events: &[String]) -> usize { + events + .iter() + .filter(|event| event.contains("function_relative_span")) + .count() +} + +fn lambda_span_query_executions(events: &[String]) -> usize { + events + .iter() + .filter(|event| event.contains("lambda_first_stmt_relative_span")) + .count() +} + +fn comment_query_executions(events: &[String]) -> usize { + events + .iter() + .filter(|event| event.contains("function_leading_comment_text")) + .count() +} + +fn nested_semantic_query_executions(events: &[String]) -> usize { + events + .iter() + .filter(|event| event.contains("nested_item_semantic_names")) + .count() +} + +fn nested_comment_query_executions(events: &[String]) -> usize { + events + .iter() + .filter(|event| event.contains("nested_item_comment_texts")) + .count() +} diff --git a/crates/parser/tests/lowering_regressions.rs b/crates/parser/tests/lowering_regressions.rs new file mode 100644 index 00000000..2775e450 --- /dev/null +++ b/crates/parser/tests/lowering_regressions.rs @@ -0,0 +1,1469 @@ +use hir::{ + anchor::DefKind, + ast::{ + SourceComment, SourceCommentKind, + function::{ + AssignOp, BinOp, ExprKind, FuncParam, FunctionMutability, FunctionVisibility, StmtKind, + }, + item::{ContractDef, ContractItem, ContractKind, FunctionDef, Item, Module, TypeAliasKind}, + ty::{FunctionTypeVisibility, TypeRefKind, TypeRefShapeKind}, + }, + diag::{AnyDiagnostic, Diagnostic}, + input::SourceFile, + span::Spanned, +}; +use solcore_parser::{parse_diagnostics, parse_file_to_hir}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl solcore_parser::Db for TestDb {} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn parse_module<'db>(db: &'db TestDb, name: &str, src: &str) -> (SourceFile, Module<'db>) { + let file = source_file(db, name, src); + (file, parse_file_to_hir(db, file).module(db)) +} + +fn diagnostics(db: &TestDb, file: SourceFile) -> Vec { + parse_diagnostics(db, file) + .iter() + .map(|diagnostic: &AnyDiagnostic| diagnostic.lower(db)) + .collect() +} + +fn top_function<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FunctionDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::FunctionDef(function) if (*function.sig(db).name.atom()).text(db) == name => { + Some(*function) + } + _ => None, + }) + .expect("top-level function") +} + +fn contract_function<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FunctionDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => contract.items(db).iter().find_map(|item| match item { + ContractItem::FunctionDef(function) + if (*function.sig(db).name.atom()).text(db) == name => + { + Some(*function) + } + _ => None, + }), + _ => None, + }) + .expect("contract function") +} + +fn contract_named<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> ContractDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) if contract.name_elem(db).atom().text(db) == name => { + Some(*contract) + } + _ => None, + }) + .expect("contract-like declaration") +} + +fn assert_comment_texts(comments: &[SourceComment], expected: &[&str]) { + assert_eq!( + comments + .iter() + .map(|comment| comment.text.as_str()) + .collect::>(), + expected + ); +} + +#[test] +fn block_comments_do_not_swallow_following_items_and_unterminated_comments_diagnose() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "block-comment-ok", + "/* **/ /* outer /* inner */ done */ function f() {}", + ); + assert_eq!( + (*top_function(&db, module, "f").sig(&db).name.atom()).text(&db), + "f" + ); + + let file = source_file(&db, "block-comment-bad", "/* unterminated\nfunction f() {}"); + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + assert!( + messages + .iter() + .any(|message| message == "unterminated block comment") + ); +} + +#[test] +fn function_hir_retains_only_directly_leading_source_comments() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "function-comments", + r#" +contract C { + // ordinary documentation + // #[(0, 1) -> 1] + /* block /* nested */ documentation */ + function add(x: word, y: word) public returns (word) { return x; } + + function body_comment() { + // this belongs to the body + } + function after_body() {} + + // separated from the declaration + + function after_blank_line() {} + + function trailing_owner() {} // trailing on the prior declaration + function after_trailing() {} +} +"#, + ); + + assert_eq!( + contract_function(&db, module, "add").leading_comments(&db), + &[ + SourceComment { + kind: SourceCommentKind::Line, + text: " ordinary documentation".to_owned(), + }, + SourceComment { + kind: SourceCommentKind::Line, + text: " #[(0, 1) -> 1]".to_owned(), + }, + SourceComment { + kind: SourceCommentKind::Block, + text: " block /* nested */ documentation ".to_owned(), + }, + ] + ); + for name in [ + "body_comment", + "after_body", + "after_blank_line", + "trailing_owner", + "after_trailing", + ] { + assert!( + contract_function(&db, module, name) + .leading_comments(&db) + .is_empty(), + "{name} unexpectedly received leading comments" + ); + } +} + +#[test] +fn hir_retains_comments_for_every_item_like_declaration() { + let db = TestDb::default(); + let (file, module) = parse_module( + &db, + "all-item-comments", + r#" +// top import +import * as dependency from dependency; +// top export +export dependency; +// top pragma +pragma solidity ^0.8.23; +// top alias +alias Alias = word; +// top enum +enum TopData { + // first constructor + First, + // second constructor + Second +} +// top trait +trait Documented { + // trait method + function describe(x: a) returns (word); +} +// top impl +impl Documented { + // impl method + function describe(x: word) returns (word) { return x; } +} +// top contract +contract C { + // contract field + value: word; + // contract alias + alias LocalAlias = word; + // contract enum + enum LocalData { + // local first constructor + LocalFirst, + // local second constructor + LocalSecond + } + // contract constructor + constructor() {} + // contract fallback + fallback() external {} + // contract function + function get() returns (word) { return value; } +} +// top function +function top() {} +"#, + ); + let diagnostics = diagnostics(&db, file); + assert!( + diagnostics.is_empty(), + "unexpected diagnostics: {diagnostics:#?}" + ); + + let expected_top_comments = [ + " top import", + " top export", + " top pragma", + " top alias", + " top enum", + " top trait", + " top impl", + " top contract", + " top function", + ]; + assert_eq!(module.items(&db).len(), expected_top_comments.len()); + for (item, expected) in module.items(&db).iter().zip(expected_top_comments) { + assert_comment_texts(item.leading_comments(&db), &[expected]); + } + + let top_adt = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::AdtDef(adt) => Some(*adt), + _ => None, + }) + .expect("top-level ADT"); + assert_eq!(top_adt.ctors_with_comments(&db).len(), 2); + assert_comment_texts( + top_adt.ctor_leading_comments(&db, 0).expect("first ctor"), + &[" first constructor"], + ); + assert_comment_texts( + top_adt.ctor_leading_comments(&db, 1).expect("second ctor"), + &[" second constructor"], + ); + + let class = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ClassDef(class) => Some(*class), + _ => None, + }) + .expect("class"); + assert_eq!(class.methods_with_comments(&db).len(), 1); + assert_comment_texts( + class.method_leading_comments(&db, 0).expect("class method"), + &[" trait method"], + ); + + let instance = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::InstanceDef(instance) => Some(*instance), + _ => None, + }) + .expect("instance"); + assert_comment_texts( + instance.methods(&db)[0].leading_comments(&db), + &[" impl method"], + ); + + let contract = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + assert_eq!(contract.fields_with_comments(&db).len(), 1); + assert_comment_texts( + contract + .field_leading_comments(&db, 0) + .expect("contract field"), + &[" contract field"], + ); + + let expected_contract_item_comments = [ + " contract alias", + " contract enum", + " contract constructor", + " contract fallback", + " contract function", + ]; + assert_eq!( + contract.items(&db).len(), + expected_contract_item_comments.len() + ); + for (item, expected) in contract + .items(&db) + .iter() + .zip(expected_contract_item_comments) + { + assert_comment_texts(item.leading_comments(&db), &[expected]); + } + + let local_adt = contract + .items(&db) + .iter() + .find_map(|item| match item { + ContractItem::AdtDef(adt) => Some(*adt), + _ => None, + }) + .expect("contract-local ADT"); + assert_eq!(local_adt.ctors_with_comments(&db).len(), 2); + assert_comment_texts( + local_adt + .ctor_leading_comments(&db, 0) + .expect("local first ctor"), + &[" local first constructor"], + ); + assert_comment_texts( + local_adt + .ctor_leading_comments(&db, 1) + .expect("local second ctor"), + &[" local second constructor"], + ); +} + +#[test] +fn item_comments_do_not_cross_blank_lines_trailing_code_or_bodies() { + let db = TestDb::default(); + let (file, module) = parse_module( + &db, + "item-comment-boundaries", + r#" +alias Owner = word; // trailing top-level comment +enum AfterTrailing {} +// separated top-level comment + +trait Boundary { + // separated method comment + + function method(x: a) returns (word); +} +contract C { + first: word; // trailing field comment + alias AfterTrailingField = word; + // separated field comment + + second: word; + enum Nested { + First, // trailing constructor comment + Second, + // separated from the constructor name by a blank line + + Third + } + function body_owner() { + // body-only comment + } + alias AfterBody = word; +} +"#, + ); + let diagnostics = diagnostics(&db, file); + assert!( + diagnostics.is_empty(), + "unexpected diagnostics: {diagnostics:#?}" + ); + assert!( + module + .items(&db) + .iter() + .all(|item| item.leading_comments(&db).is_empty()) + ); + + let class = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ClassDef(class) => Some(*class), + _ => None, + }) + .expect("class"); + assert!( + class + .method_leading_comments(&db, 0) + .expect("class method") + .is_empty() + ); + + let contract = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + assert!( + contract + .fields_with_comments(&db) + .all(|(_, comments)| comments.is_empty()) + ); + assert!( + contract + .items(&db) + .iter() + .all(|item| item.leading_comments(&db).is_empty()) + ); + let adt = contract + .items(&db) + .iter() + .find_map(|item| match item { + ContractItem::AdtDef(adt) => Some(*adt), + _ => None, + }) + .expect("nested ADT"); + assert!( + adt.ctors_with_comments(&db) + .all(|(_, comments)| comments.is_empty()) + ); +} + +#[test] +fn recovery_items_retain_comments_without_leaking_to_following_items() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "recovery-item-comments", + r#" +// invalid top-level item +unknown top; +function valid_top() {} +contract C { + // invalid contract item + unknown nested; + function valid_nested() {} +} +"#, + ); + + let top_error = module.items(&db)[0]; + assert!(matches!(top_error, Item::Error { .. })); + assert_comment_texts( + top_error.leading_comments(&db), + &[" invalid top-level item"], + ); + assert!( + top_function(&db, module, "valid_top") + .leading_comments(&db) + .is_empty() + ); + + let contract = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + let nested_error = contract.items(&db)[0]; + assert!(matches!(nested_error, ContractItem::Error { .. })); + assert_comment_texts( + nested_error.leading_comments(&db), + &[" invalid contract item"], + ); + assert!( + contract_function(&db, module, "valid_nested") + .leading_comments(&db) + .is_empty() + ); +} + +#[test] +fn equivalent_type_and_predicate_refs_share_semantic_shapes_without_sharing_occurrences() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "type-ref-shapes", + "trait C {} + function a(x: word) {} + function b(y: word) {} + function c(x: t) where t: C {} + function d(x: t) where t: C {}", + ); + + let a = top_function(&db, module, "a"); + let b = top_function(&db, module, "b"); + let a_ty = match &a.sig(&db).params.atom()[0] { + FuncParam::Typed { ty, .. } => *ty, + other => panic!("unexpected param: {other:?}"), + }; + let b_ty = match &b.sig(&db).params.atom()[0] { + FuncParam::Typed { ty, .. } => *ty, + other => panic!("unexpected param: {other:?}"), + }; + assert_ne!(a_ty, b_ty); + assert_eq!(a_ty.semantic_shape(), b_ty.semantic_shape()); + + let c = top_function(&db, module, "c"); + let d = top_function(&db, module, "d"); + let c_pred = c.sig(&db).preds[0]; + let d_pred = d.sig(&db).preds[0]; + assert_ne!(c_pred, d_pred); + assert_eq!(c_pred.semantic_shape(), d_pred.semantic_shape()); +} + +#[test] +fn expression_statements_stay_expressions_and_explicit_returns_stay_returns() { + let db = TestDb::default(); + let (file, module) = parse_module( + &db, + "explicit-return", + "function expression() returns (word) { 1; } + function explicit() returns (word) { return 1; }", + ); + assert!( + diagnostics(&db, file).is_empty(), + "unexpected parse diagnostics" + ); + + let expression = top_function(&db, module, "expression"); + let expression_body = expression.body(&db).expect("body"); + let expression_stmt = expression_body + .stmts(&db) + .get(expression_body.top_level_stmts(&db)[0]); + assert!(matches!(&expression_stmt.kind, StmtKind::Expr(_))); + + let explicit = top_function(&db, module, "explicit"); + let explicit_body = explicit.body(&db).expect("body"); + let explicit_stmt = explicit_body + .stmts(&db) + .get(explicit_body.top_level_stmts(&db)[0]); + assert!(matches!(&explicit_stmt.kind, StmtKind::Return(Some(_)))); +} + +#[test] +fn contract_fields_can_be_interleaved_and_have_initializers() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "contract-fields", + "contract C { + function f() {} + x: word = 1; + }", + ); + + let contract = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) => Some(*contract), + _ => None, + }) + .expect("contract"); + assert_eq!(contract.fields(&db).len(), 1); + assert!(contract.fields(&db)[0].init().is_some()); + assert_eq!( + contract + .items(&db) + .iter() + .filter(|item| matches!(item, ContractItem::FunctionDef(_))) + .count(), + 1 + ); +} + +#[test] +fn top_level_recovery_resumes_at_next_item_and_preserves_body_errors() { + let db = TestDb::default(); + let src = "import core.math +function bad() { + let x = ; + return 1; +} +function good() {}"; + let (file, module) = parse_module(&db, "top-level-resync", src); + + assert!(top_function(&db, module, "bad").body(&db).is_some()); + assert!(top_function(&db, module, "good").body(&db).is_some()); + + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + assert!( + messages + .iter() + .any(|message| { message.contains("import declaration requires trailing `;`") }) + ); + assert!(messages.iter().any(|message| { + message.contains("while parsing expression") + || message.contains("while parsing statement") + || message.contains("unexpected `let`") + || message.contains("unexpected `;`") + })); +} + +#[test] +fn function_types_preserve_source_arity_and_explicit_tuple_domains() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "function-types", + "alias F = function(word) returns (function(word) returns (bool)); + alias G = function(word, bool) returns (uint); + alias H = function((word, bool)) returns (uint); + alias I = function() returns (uint);", + ); + let aliases = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .collect::>(); + + let f = aliases[0].ty(&db); + let TypeRefKind::Fn { + params, + visibility, + mutability, + ret, + .. + } = f.kind(&db) + else { + panic!("F should be a function type"); + }; + assert_eq!(params.atom().len(), 1); + assert!(visibility.is_none()); + assert!(mutability.is_none()); + assert!(matches!(ret.kind(&db), TypeRefKind::Fn { .. })); + + let g = aliases[1].ty(&db); + let TypeRefKind::Fn { params, .. } = g.kind(&db) else { + panic!("G should be a function type"); + }; + assert_eq!(params.atom().len(), 2); + assert!( + params + .atom() + .iter() + .all(|param| !matches!(param.kind(&db), TypeRefKind::Tuple { .. })) + ); + + let h = aliases[2].ty(&db); + let TypeRefKind::Fn { params, .. } = h.kind(&db) else { + panic!("H should be a function type"); + }; + assert_eq!(params.atom().len(), 1); + assert!(matches!( + params.atom()[0].kind(&db), + TypeRefKind::Tuple { .. } + )); + + let i = aliases[3].ty(&db); + let TypeRefKind::Fn { params, .. } = i.kind(&db) else { + panic!("I should be a function type"); + }; + assert!(params.atom().is_empty()); +} + +#[test] +fn function_type_qualifiers_preserve_spans_shapes_and_fingerprints() { + let db = TestDb::default(); + let src = "alias A = function(word) internal view returns (bool); +alias B = function(word) internal view returns (bool); +alias C = function(word) external view returns (bool); +alias D = function(word) external pure returns (bool); +trait Marker {} +impl Marker {} +impl Marker {}"; + let (file, module) = parse_module(&db, "function-type-qualifiers", src); + assert!( + diagnostics(&db, file).is_empty(), + "function-type qualifiers should parse without diagnostics" + ); + + let aliases = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .collect::>(); + assert_eq!(aliases.len(), 4); + + let a_ty = aliases[0].ty(&db); + let TypeRefKind::Fn { + span, + params, + visibility: Some(visibility), + mutability: Some(mutability), + .. + } = a_ty.kind(&db) + else { + panic!("A should retain its complete qualified function type"); + }; + assert_eq!( + *visibility.atom(), + FunctionTypeVisibility::Internal, + "function-type visibility must use its restricted enum" + ); + assert_eq!(*mutability.atom(), FunctionMutability::View); + + let type_text = "function(word) internal view returns (bool)"; + let type_start = src.find(type_text).expect("qualified function type") as u32; + let type_abs = span.resolve_to_absolute(&db); + assert_eq!(type_abs.start().as_u32(), type_start); + assert_eq!( + type_abs.end().as_u32(), + type_start + type_text.len() as u32, + "the HIR type span must begin at `function`, not at its parameter list" + ); + + let params_abs = params.span(&db).resolve_to_absolute(&db); + let params_start = src.find("(word)").expect("function domain") as u32; + assert_eq!(params_abs.start().as_u32(), params_start); + assert_eq!( + params_abs.end().as_u32(), + params_start + "(word)".len() as u32 + ); + + let visibility_abs = visibility.span(&db).resolve_to_absolute(&db); + let visibility_start = src.find("internal").expect("visibility keyword") as u32; + assert_eq!(visibility_abs.start().as_u32(), visibility_start); + assert_eq!( + visibility_abs.end().as_u32(), + visibility_start + "internal".len() as u32 + ); + + let mutability_abs = mutability.span(&db).resolve_to_absolute(&db); + let mutability_start = src.find("view").expect("mutability keyword") as u32; + assert_eq!(mutability_abs.start().as_u32(), mutability_start); + assert_eq!( + mutability_abs.end().as_u32(), + mutability_start + "view".len() as u32 + ); + + let TypeRefShapeKind::Fn { + visibility, + mutability, + .. + } = a_ty.semantic_shape().kind(&db) + else { + panic!("A should have a function-type shape"); + }; + assert_eq!(*visibility, Some(FunctionTypeVisibility::Internal)); + assert_eq!(*mutability, Some(FunctionMutability::View)); + assert_eq!( + a_ty.semantic_shape(), + aliases[1].ty(&db).semantic_shape(), + "equal qualifier values should share a span-free shape" + ); + assert_ne!( + a_ty.semantic_shape(), + aliases[2].ty(&db).semantic_shape(), + "visibility must participate in function-type shape identity" + ); + assert_ne!( + aliases[2].ty(&db).semantic_shape(), + aliases[3].ty(&db).semantic_shape(), + "mutability must participate in function-type shape identity" + ); + let TypeRefKind::Fn { + visibility: Some(visibility), + mutability: Some(mutability), + .. + } = aliases[3].ty(&db).kind(&db) + else { + panic!("D should retain both function-type qualifiers"); + }; + assert_eq!(*visibility.atom(), FunctionTypeVisibility::External); + assert_eq!(*mutability.atom(), FunctionMutability::Pure); + + let mut fingerprints = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::InstanceDef(instance) => instance.def_id_value(&db).fingerprint(&db), + _ => None, + }) + .collect::>(); + fingerprints.sort(); + let internal = "fn[internal view](word)->bool"; + let external = "fn[external view](word)->bool"; + let mut expected = vec![ + format!("pred[1]|{}:{internal}", internal.len()), + format!("pred[1]|{}:{external}", external.len()), + ]; + expected.sort(); + assert_eq!( + fingerprints, expected, + "fingerprints must encode qualifiers in canonical visibility/mutability order" + ); +} + +#[test] +fn function_type_qualifiers_survive_conversion_target_lowering() { + let db = TestDb::default(); + let src = "function convert(candidate: word) { + return candidate as function(word) external payable returns (bool); +}"; + let (file, module) = parse_module(&db, "function-type-conversion", src); + assert!( + diagnostics(&db, file).is_empty(), + "qualified function conversion should parse" + ); + + let convert = top_function(&db, module, "convert"); + let body = convert.body(&db).expect("conversion body"); + let stmt = body.stmts(&db).get(body.top_level_stmts(&db)[0]); + let StmtKind::Return(Some(expr)) = stmt.kind else { + panic!("expected conversion return"); + }; + let ExprKind::Conversion { ty, .. } = body.exprs(&db).get(expr).kind else { + panic!("expected source `as` conversion"); + }; + let TypeRefKind::Fn { + span, + visibility: Some(visibility), + mutability: Some(mutability), + .. + } = ty.kind(&db) + else { + panic!("conversion target should retain its function-type qualifiers"); + }; + assert_eq!(*visibility.atom(), FunctionTypeVisibility::External); + assert_eq!(*mutability.atom(), FunctionMutability::Payable); + + let type_text = "function(word) external payable returns (bool)"; + let expected_start = src.find(type_text).expect("conversion target") as u32; + let absolute = span.resolve_to_absolute(&db); + assert_eq!(absolute.start().as_u32(), expected_start); + assert_eq!( + absolute.end().as_u32(), + expected_start + type_text.len() as u32 + ); +} + +#[test] +fn function_types_reject_declaration_only_and_noncanonical_qualifiers() { + for (name, ty) in [ + ("public", "function(word) public returns (bool)"), + ("private", "function(word) private returns (bool)"), + ("data-location", "function(word) memory returns (bool)"), + ( + "modifier-order", + "function(word) view external returns (bool)", + ), + ] { + let db = TestDb::default(); + let src = format!("// migrate-syntax: keep-legacy-negative\nalias Callback = {ty};\n"); + let (file, _) = parse_module(&db, name, &src); + assert!( + !diagnostics(&db, file).is_empty(), + "function type `{ty}` must not be accepted as canonical new syntax" + ); + } +} + +#[test] +fn alias_and_value_type_declarations_remain_distinct_in_hir() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "type-declaration-kinds", + "alias WordAlias = word; type Wad is word;", + ); + let declarations = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::TypeAlias(declaration) => Some(*declaration), + _ => None, + }) + .collect::>(); + + assert_eq!(declarations[0].kind(&db), TypeAliasKind::Transparent); + assert_eq!( + declarations[0].def_id_value(&db).kind(&db), + DefKind::TypeAlias + ); + assert_eq!(declarations[1].kind(&db), TypeAliasKind::ValueType); + assert_eq!( + declarations[1].def_id_value(&db).kind(&db), + DefKind::ValueType + ); +} + +#[test] +fn fixed_array_lengths_survive_parsing_and_hir_lowering() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "fixed-array-types", + "alias Nested = word[4][2]; + alias Mixed = word[4][]; + alias FixedFour = word[4]; + alias FixedFive = word[5]; + alias HexFour = word[0x4];", + ); + let aliases = module + .items(&db) + .iter() + .filter_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .collect::>(); + + let TypeRefKind::FixedArray { + element, length: 2, .. + } = aliases[0].ty(&db).kind(&db) + else { + panic!("outer fixed-array length should survive HIR lowering"); + }; + assert!(matches!( + element.kind(&db), + TypeRefKind::FixedArray { length: 4, .. } + )); + + let TypeRefKind::Named { name, args, .. } = aliases[1].ty(&db).kind(&db) else { + panic!("dynamic array should retain its nominal DynArray representation"); + }; + assert_eq!((*name.atom()).text(&db), "DynArray"); + assert!(matches!( + args.atom().as_slice(), + [element] if matches!( + element.kind(&db), + TypeRefKind::FixedArray { length: 4, .. } + ) + )); + + assert_ne!( + aliases[2].ty(&db).semantic_shape(), + aliases[3].ty(&db).semantic_shape(), + "fixed-array lengths must participate in HIR type identity" + ); + assert!(matches!( + aliases[4].ty(&db).kind(&db), + TypeRefKind::FixedArray { length: 4, .. } + )); + assert_eq!( + aliases[2].ty(&db).semantic_shape(), + aliases[4].ty(&db).semantic_shape(), + "decimal and hexadecimal spellings of the same length must share one type shape" + ); +} + +#[test] +fn fixed_array_lengths_reject_zero_and_u64_overflow() { + let db = TestDb::default(); + let (file, _) = parse_module( + &db, + "invalid-fixed-array-lengths", + "// migrate-syntax: keep-legacy-negative\n\ + alias Zero = word[0]; alias Huge = word[18446744073709551616];", + ); + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + + assert!( + messages + .iter() + .any(|message| message.contains("fixed array length must be greater than zero")), + "{messages:#?}" + ); + assert!( + messages + .iter() + .any(|message| message.contains("exceeds the supported u64 range")), + "{messages:#?}" + ); +} + +#[test] +fn type_and_predicate_argument_list_spans_are_precise() { + let db = TestDb::default(); + let src = "trait C {} +alias T = Map; +function f(x: t) where t: C {}"; + let (_, module) = parse_module(&db, "precise-type-spans", src); + + let alias = module + .items(&db) + .iter() + .find_map(|item| match item { + Item::TypeAlias(alias) => Some(*alias), + _ => None, + }) + .expect("type alias"); + let TypeRefKind::Named { args, .. } = alias.ty(&db).kind(&db) else { + panic!("alias target should be named"); + }; + let args_abs = args.span(&db).resolve_to_absolute(&db); + let expected_args_start = src.find("").expect("type args") as u32; + assert_eq!(args_abs.start().as_u32(), expected_args_start); + assert_eq!( + args_abs.end().as_u32(), + expected_args_start + "".len() as u32 + ); + + let function = top_function(&db, module, "f"); + let pred = function.sig(&db).preds[0].kind(&db); + let pred_args_abs = pred.args.span(&db).resolve_to_absolute(&db); + let expected_pred_start = src.find("").expect("predicate args") as u32; + assert_eq!(pred_args_abs.start().as_u32(), expected_pred_start); + assert_eq!( + pred_args_abs.end().as_u32(), + expected_pred_start + "".len() as u32 + ); +} + +#[test] +fn ternary_expression_lowers_to_conditional_expression() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "ternary", + "function f(x: bool) returns (word) { return x ? 1 : 0; }", + ); + let function = top_function(&db, module, "f"); + let body = function.body(&db).expect("body"); + let stmt = body.stmts(&db).get(body.top_level_stmts(&db)[0]); + let StmtKind::Return(Some(expr_id)) = &stmt.kind else { + panic!("expected return with expression"); + }; + assert!(matches!( + &body.exprs(&db).get(*expr_id).kind, + ExprKind::If { .. } + )); +} + +#[test] +fn surface_conversion_and_lowering_ascription_remain_distinct() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "conversion-vs-ascription", + "function convert(x: word) returns (word) { return x as word; } + function destructure(x: word, y: bool) { + let (a, b): (word, bool) = (x, y); + }", + ); + + let convert = top_function(&db, module, "convert"); + let body = convert.body(&db).expect("conversion body"); + let stmt = body.stmts(&db).get(body.top_level_stmts(&db)[0]); + let StmtKind::Return(Some(expr)) = stmt.kind else { + panic!("expected conversion return"); + }; + assert!(matches!( + body.exprs(&db).get(expr).kind, + ExprKind::Conversion { .. } + )); + + let destructure = top_function(&db, module, "destructure"); + let body = destructure.body(&db).expect("destructure body"); + let stmt = body.stmts(&db).get(body.top_level_stmts(&db)[0]); + let StmtKind::Match { scrutinees, .. } = &stmt.kind else { + panic!("typed tuple binding should lower through a match"); + }; + assert!(matches!( + body.exprs(&db).get(scrutinees[0]).kind, + ExprKind::TypeAscription { .. } + )); +} + +#[test] +fn compound_assignments_lower_through_binary_operator_calls() { + let db = TestDb::default(); + let (_, module) = parse_module( + &db, + "compound-assignments", + "function f(x: word, y: word) {\n\ + x += y;\n\ + x -= y;\n\ + x ^= y;\n\ + x &= y;\n\ + x |= y;\n\ + x %= y;\n\ + }", + ); + let function = top_function(&db, module, "f"); + let body = function.body(&db).expect("body"); + let expected = [ + BinOp::Add, + BinOp::Sub, + BinOp::BitXor, + BinOp::BitAnd, + BinOp::BitOr, + BinOp::Mod, + ]; + + for (stmt_id, expected_op) in body.top_level_stmts(&db).iter().zip(expected) { + let stmt = body.stmts(&db).get(*stmt_id); + let StmtKind::Assign { + op: AssignOp::Plain, + rhs, + .. + } = &stmt.kind + else { + panic!("compound assignment should lower to plain assignment"); + }; + assert!(matches!( + &body.exprs(&db).get(*rhs).kind, + ExprKind::BinOp { op, .. } if *op.atom() == expected_op + )); + } +} + +#[test] +fn boolean_and_fallback_keywords_are_rejected_as_declaration_names() { + let db = TestDb::default(); + let contexts = [ + "function {keyword}() {}", + "function f({keyword}: word) {}", + "function f() returns ({keyword}: word) {}", + "function f() { let {keyword}: word = 0; }", + "struct {keyword} { value: word; }", + "struct S { {keyword}: word; }", + "enum E { {keyword} }", + "type {keyword} is word;", + "contract {keyword} {}", + "contract C { {keyword}: word; }", + "alias A<{keyword}> = word;", + "import * as {keyword} from source;", + ]; + + for keyword in ["true", "false", "fallback"] { + let expected = format!("`{keyword}` is reserved and cannot be used as an identifier"); + for (index, context) in contexts.iter().enumerate() { + let source = context.replace("{keyword}", keyword); + let file = source_file(&db, &format!("reserved-{keyword}-{index}"), &source); + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + assert!( + messages.iter().any(|message| message == &expected), + "missing reserved-name diagnostic for `{source}`: {messages:#?}" + ); + } + } +} + +#[test] +fn boolean_values_patterns_and_special_fallback_remain_valid() { + let db = TestDb::default(); + let source = r#" +contract Keywords { + fallback() external {} +} + +function negate(value: bool) returns (bool) { + match (value) { + case true { return false; } + case false { return true; } + } +} + +function fallbackHandler() {} +"#; + let file = source_file(&db, "reserved-keyword-positive", source); + let diagnostics = diagnostics(&db, file); + assert!( + diagnostics.is_empty(), + "keyword literals and the special fallback declaration should parse: {diagnostics:#?}" + ); +} + +#[test] +fn contract_shell_kinds_and_prototype_bodies_survive_lowering() { + let db = TestDb::default(); + let source = r#" +contract C { + function run() {} +} + +interface I { + function read(key: word) external view returns (word); +} + +library L { + function add(x: word, y: word) internal pure returns (word) { return x + y; } +} +"#; + let (file, module) = parse_module(&db, "contract-shell-kinds", source); + assert!(diagnostics(&db, file).is_empty()); + + let contract = contract_named(&db, module, "C"); + let interface = contract_named(&db, module, "I"); + let library = contract_named(&db, module, "L"); + assert_eq!(contract.kind(&db), ContractKind::Contract); + assert_eq!(interface.kind(&db), ContractKind::Interface); + assert_eq!(library.kind(&db), ContractKind::Library); + + let run = contract_function(&db, module, "run"); + assert_eq!(run.sig(&db).visibility_kind(), None); + assert_eq!(run.sig(&db).mutability_kind(), None); + let read = contract_function(&db, module, "read"); + assert_eq!( + read.sig(&db).visibility_kind(), + Some(FunctionVisibility::External) + ); + assert_eq!( + read.sig(&db).mutability_kind(), + Some(FunctionMutability::View) + ); + let add = contract_function(&db, module, "add"); + assert_eq!( + add.sig(&db).visibility_kind(), + Some(FunctionVisibility::Internal) + ); + assert_eq!( + add.sig(&db).mutability_kind(), + Some(FunctionMutability::Pure) + ); + + let has_body = |declaration: ContractDef<'_>| { + declaration + .items(&db) + .iter() + .find_map(|item| match item { + ContractItem::FunctionDef(function) => Some(function.body(&db)), + _ => None, + }) + .expect("function member") + .is_some() + }; + assert!(has_body(contract)); + assert!(!has_body(interface)); + assert!(has_body(library)); +} + +#[test] +fn function_declaration_modifiers_survive_lowering_with_keyword_spans() { + let db = TestDb::default(); + let source = r#" +function public_fn() public {} +function external_fn() external {} +function internal_fn() internal {} +function private_fn() private {} +function pure_fn() pure {} +function view_fn() view {} +function payable_fn() payable {} +"#; + let (file, module) = parse_module(&db, "function-modifiers", source); + assert!(diagnostics(&db, file).is_empty()); + + for (name, expected) in [ + ("public_fn", FunctionVisibility::Public), + ("external_fn", FunctionVisibility::External), + ("internal_fn", FunctionVisibility::Internal), + ("private_fn", FunctionVisibility::Private), + ] { + let visibility = top_function(&db, module, name) + .sig(&db) + .visibility + .expect("visibility modifier"); + assert_eq!(*visibility.atom(), expected); + let absolute = visibility.span(&db).resolve_to_absolute(&db); + assert_eq!( + &source[absolute.start().as_usize()..absolute.end().as_usize()], + expected.keyword() + ); + } + + for (name, expected) in [ + ("pure_fn", FunctionMutability::Pure), + ("view_fn", FunctionMutability::View), + ("payable_fn", FunctionMutability::Payable), + ] { + let mutability = top_function(&db, module, name) + .sig(&db) + .mutability + .expect("mutability modifier"); + assert_eq!(*mutability.atom(), expected); + let absolute = mutability.span(&db).resolve_to_absolute(&db); + assert_eq!( + &source[absolute.start().as_usize()..absolute.end().as_usize()], + expected.keyword() + ); + } +} + +#[test] +fn duplicate_conflicting_and_invalid_function_modifiers_are_rejected() { + let db = TestDb::default(); + let cases = [ + ( + "duplicate-visibility", + "// migrate-syntax: keep-legacy-negative\nfunction f() public public {}", + "duplicate function visibility `public`; `public` was already specified", + ), + ( + "conflicting-visibility", + "function f() public external {}", + "conflicting function visibility `external`; `public` was already specified", + ), + ( + "duplicate-mutability", + "// migrate-syntax: keep-legacy-negative\nfunction f() pure pure {}", + "duplicate function mutability `pure`; `pure` was already specified", + ), + ( + "conflicting-mutability", + "function f() view payable {}", + "conflicting function mutability `payable`; `view` was already specified", + ), + ( + "constructor-visibility", + "contract C { constructor() internal {} }", + "`internal` is not allowed on constructor", + ), + ( + "constructor-mutability", + "contract C { constructor() pure {} }", + "`pure` is not allowed on constructor", + ), + ( + "fallback-visibility", + "contract C { fallback() private {} }", + "`private` is not allowed on fallback", + ), + ( + "fallback-mutability", + "contract C { fallback() view {} }", + "`view` is not allowed on fallback", + ), + ( + "interface-visibility", + "interface I { function f() public; }", + "interface functions must be declared `external`", + ), + ]; + + for (name, source, expected) in cases { + let file = source_file(&db, name, source); + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + assert!( + messages.iter().any(|message| message == expected), + "missing `{expected}` for `{source}`: {messages:#?}" + ); + } +} + +#[test] +fn contract_shells_reject_members_that_cannot_be_implemented() { + let db = TestDb::default(); + let cases = [ + ( + "interface-field", + "interface Bad { value: word; }", + "interface declarations cannot contain storage fields", + ), + ( + "interface-body", + "interface Bad { function f() external {} }", + "interface functions must be prototypes ending in `;`", + ), + ( + "interface-constructor", + "interface Bad { constructor() {} }", + "interface declarations cannot contain constructor functions", + ), + ( + "interface-fallback", + "interface Bad { fallback() external {} }", + "interface declarations cannot contain fallback functions", + ), + ( + "library-field", + "library Bad { value: word; }", + "library declarations cannot contain storage fields", + ), + ( + "library-prototype", + "library Bad { function f(); }", + "library functions must provide a body", + ), + ( + "library-constructor", + "library Bad { constructor() {} }", + "library declarations cannot contain constructor functions", + ), + ( + "library-fallback", + "library Bad { fallback() external {} }", + "library declarations cannot contain fallback functions", + ), + ( + "contract-prototype", + "contract Bad { function f(); }", + "contract functions must provide a body", + ), + ]; + + for (name, source, expected) in cases { + let file = source_file(&db, name, source); + let messages = diagnostics(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect::>(); + assert!( + messages.iter().any(|message| message == expected), + "missing `{expected}` for `{source}`: {messages:#?}" + ); + } +} diff --git a/crates/parser/tests/nameres.rs b/crates/parser/tests/nameres.rs new file mode 100644 index 00000000..e26f1e13 --- /dev/null +++ b/crates/parser/tests/nameres.rs @@ -0,0 +1,840 @@ +use hir::{ + ast::{ + function::{ExprKind, FuncBody}, + item::{ContractItem, FunctionDef, Item, Module}, + }, + diag::Diagnostic, + input::SourceFile, + nameres::{ + DefResolutionKind, EmptyImportedNames, ImportedNames, ModuleRef, NameresDiagnostic, + NameresDiagnosticPolicy, Namespace, Resolution, UndefinedNameKind, item_scope, + resolve_module, resolve_module_with_imports_and_policy, + }, +}; +use solcore_parser::{parse_diagnostics, parse_file_to_hir}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl solcore_parser::Db for TestDb {} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid url"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn parse_module<'db>(db: &'db TestDb, src: &str) -> Module<'db> { + let file = source_file(db, "nameres", src); + parse_file_to_hir(db, file).module(db) +} + +fn parse_and_module<'db>(db: &'db TestDb, name: &str, src: &str) -> (SourceFile, Module<'db>) { + let file = source_file(db, name, src); + let module = parse_file_to_hir(db, file).module(db); + (file, module) +} + +fn function_name<'db>(db: &'db TestDb, function: FunctionDef<'db>) -> &'db str { + (*function.sig(db).name.atom()).text(db) +} + +fn top_function<'db>(db: &'db TestDb, module: Module<'db>, name: &str) -> FunctionDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::FunctionDef(function) if function_name(db, *function) == name => Some(*function), + _ => None, + }) + .expect("top-level function") +} + +fn contract_function<'db>( + db: &'db TestDb, + module: Module<'db>, + contract_name: &str, + function_name_: &str, +) -> FunctionDef<'db> { + module + .items(db) + .iter() + .find_map(|item| match item { + Item::ContractDef(contract) + if (*contract.name_elem(db).atom()).text(db) == contract_name => + { + contract.items(db).iter().find_map(|item| match item { + ContractItem::FunctionDef(function) + if function_name(db, *function) == function_name_ => + { + Some(*function) + } + _ => None, + }) + } + _ => None, + }) + .expect("contract function") +} + +fn diagnostics<'db>(db: &'db TestDb, module: Module<'db>) -> Vec { + resolve_module(db, module) + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect() +} + +fn diagnostic_codes(db: &TestDb, module: Module<'_>) -> Vec { + diagnostics(db, module) + .iter() + .filter_map(|diagnostic| diagnostic.code.clone()) + .collect() +} + +struct ModuleOnlyImports<'db> { + owner: Module<'db>, +} + +struct UnknownWildcardImports; + +impl<'db> ImportedNames<'db> for UnknownWildcardImports { + fn imported( + &self, + _db: &'db dyn hir::Db, + _namespace: Namespace, + _name: &str, + ) -> Option> { + None + } + + fn may_contain_unknown_unqualified( + &self, + _db: &'db dyn hir::Db, + _namespace: Namespace, + _name: &str, + ) -> bool { + true + } +} + +impl<'db> ImportedNames<'db> for ModuleOnlyImports<'db> { + fn imported( + &self, + db: &'db dyn hir::Db, + namespace: Namespace, + name: &str, + ) -> Option> { + (namespace == Namespace::Module && name == "math").then(|| { + Resolution::Module(ModuleRef { + owner: self.owner.def_id_value(db), + name: name.to_owned(), + }) + }) + } +} + +#[test] +fn parse_recovery_suppression_policy_silences_name_lookup_cascades() { + let cases = [ + ( + "body_expr_error", + "function f() returns (word) { + let x = ; + return missing; + }", + ), + ( + "lost_function_signature", + "lost(x: word) returns (word) { return 0; } + function caller() returns (word) { return lost(0); }", + ), + ( + "broken_import", + "impoort util; + function caller() returns (word) { return missing; }", + ), + ( + "broken_type_annotation", + "typeish Alias = word; + function caller(x: Alias) returns (word) { return 0; }", + ), + ( + "top_level_item_error", + "function first() {} + unknown nonsense tokens + function second() {} + function caller() returns (word) { return missing; }", + ), + ( + "broken_contract_member", + "contract C { + broken nonsense; + function get() returns (word) { return broken; } + }", + ), + ]; + + for (name, src) in cases { + let db = TestDb::default(); + let (file, module) = parse_and_module(&db, name, src); + let parse_count = parse_diagnostics(&db, file).len(); + assert!(parse_count > 0, "probe `{name}` should have parse errors"); + let scope = item_scope(&db, module); + let imports = EmptyImportedNames; + let resolution = resolve_module_with_imports_and_policy( + &db, + module, + scope, + &imports, + NameresDiagnosticPolicy::SuppressForParseErrors, + ); + assert!( + resolution.diagnostics.is_empty(), + "parse-broken probe `{name}` should not publish nameres diagnostics" + ); + if name == "body_expr_error" { + assert!( + resolution + .bodies + .iter() + .flat_map(|map| &map.exprs) + .any(|entry| { + matches!(&entry.body.exprs(&db).get(entry.expr).kind, ExprKind::Error) + && matches!(entry.resolution, Resolution::Err) + }), + "recovered expression errors should resolve to Resolution::Err" + ); + } + } +} + +#[test] +fn undefined_name_kind_distinguishes_bare_terms_from_path_lookups() { + let db = TestDb::default(); + let (file, module) = parse_and_module( + &db, + "undefined_name_kinds", + "enum Local { Present } + function bare() returns (word) { return missing; } + function qualified() returns (word) { return math.value(); } + function ctorExpr() returns (word) { return Option.Some(0); } + function ctorPat(x: word) returns (word) { + match (x) { case Option.Some(y) { return y; } default { return 0; } } + } + function valueMember(x: word) returns (word) { return x.absent; } + function member() returns (word) { return Local.absent; }", + ); + assert!(parse_diagnostics(&db, file).is_empty()); + + let resolution = resolve_module(&db, module); + let diagnostics = resolution + .diagnostics + .iter() + .filter_map(|diagnostic| match diagnostic { + NameresDiagnostic::UndefinedName { name, kind, .. } => { + Some((name.as_str(), kind.clone())) + } + _ => None, + }) + .collect::>(); + + assert_eq!( + diagnostics, + [ + ("missing", UndefinedNameKind::Term), + ( + "math", + UndefinedNameKind::ModuleQualifier { + access_path: "math.value".to_owned(), + }, + ), + ( + "Option", + UndefinedNameKind::ModuleQualifier { + access_path: "Option.Some".to_owned(), + }, + ), + ( + "Option.Some", + UndefinedNameKind::QualifiedConstructor { + access_path: "Option.Some".to_owned(), + }, + ), + ("absent", UndefinedNameKind::Field), + ] + ); +} + +#[test] +fn missing_resolved_module_member_has_qualified_lookup_context() { + let db = TestDb::default(); + let (file, module) = parse_and_module( + &db, + "missing_module_member", + "enum Local { Present } + function missing() returns (word) { + let fromModule = math.value(); + return Local.absent; + }", + ); + assert!(parse_diagnostics(&db, file).is_empty()); + + let scope = item_scope(&db, module); + let imports = ModuleOnlyImports { owner: module }; + let resolution = resolve_module_with_imports_and_policy( + &db, + module, + scope, + &imports, + NameresDiagnosticPolicy::Emit, + ); + let diagnostics = resolution + .diagnostics + .iter() + .filter_map(|diagnostic| match diagnostic { + NameresDiagnostic::UndefinedName { name, kind, .. } => { + Some((name.as_str(), kind.clone())) + } + _ => None, + }) + .collect::>(); + + assert_eq!( + diagnostics, + [ + ( + "value", + UndefinedNameKind::ModuleMember { + access_path: "math.value".to_owned(), + }, + ), + ("absent", UndefinedNameKind::Field), + ] + ); +} + +#[test] +fn missing_constructor_on_resolved_type_is_not_an_import_context() { + let db = TestDb::default(); + let (file, module) = parse_and_module( + &db, + "missing_local_constructor", + "enum Option { None } + function missing(value: Option) returns (word) { + match (value) { case Option.Some { return 1; } default { return 0; } } + }", + ); + assert!(parse_diagnostics(&db, file).is_empty()); + + let resolution = resolve_module(&db, module); + let diagnostics = resolution + .diagnostics + .iter() + .filter_map(|diagnostic| match diagnostic { + NameresDiagnostic::UndefinedName { name, kind, .. } => { + Some((name.as_str(), kind.clone())) + } + _ => None, + }) + .collect::>(); + + assert_eq!(diagnostics, [("Option.Some", UndefinedNameKind::Field)]); +} + +fn body_map<'db>( + db: &'db TestDb, + module: Module<'db>, + body: FuncBody<'db>, +) -> hir::nameres::BodyResolutionMap<'db> { + resolve_module(db, module) + .bodies + .into_iter() + .find(|map| { + map.exprs.iter().any(|entry| entry.body == body) + || map.stmt_bindings.iter().any(|entry| entry.body == body) + || map.pats.iter().any(|entry| entry.body == body) + }) + .expect("body map") +} + +fn ident_resolutions<'db>( + db: &'db TestDb, + body: FuncBody<'db>, + map: &hir::nameres::BodyResolutionMap<'db>, +) -> Vec<(&'db str, Resolution<'db>)> { + map.exprs + .iter() + .filter(|entry| entry.body == body) + .filter_map(|entry| match &body.exprs(db).get(entry.expr).kind { + ExprKind::Ident(name) => Some(((*name.atom()).text(db), entry.resolution.clone())), + _ => None, + }) + .collect() +} + +#[test] +fn let_initializer_resolves_before_binder_and_then_shadows() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function f(x: word) returns (word) { + let x = x; + return x; + }", + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + let function = top_function(&db, module, "f"); + let body = function.body(&db).expect("body"); + let map = body_map(&db, module, body); + let events = ident_resolutions(&db, body, &map); + + assert_eq!( + events.iter().map(|(name, _)| *name).collect::>(), + ["x", "x"] + ); + assert!(matches!(events[0].1, Resolution::Param(_))); + assert!(matches!(events[1].1, Resolution::Local(_))); +} + +#[test] +fn named_return_is_preserved_and_resolves_as_a_body_local() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function named(x: word) returns (result: word) { + result = x; + return result; + }", + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + let function = top_function(&db, module, "named"); + let sig = function.sig(&db); + assert_eq!(sig.ret_names.len(), 1); + assert_eq!( + sig.ret_names[0] + .as_ref() + .map(|name| (*name.atom()).text(&db)), + Some("result") + ); + + let body = function.body(&db).expect("body"); + let map = body_map(&db, module, body); + let events = ident_resolutions(&db, body, &map); + let result_events = events + .iter() + .filter(|(name, _)| *name == "result") + .collect::>(); + assert_eq!(result_events.len(), 2); + assert!( + result_events + .iter() + .all(|(_, resolution)| matches!(resolution, Resolution::Local(_))) + ); +} + +#[test] +fn explicit_blocks_scope_locals_but_for_body_lets_leak() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function f(x: word) returns (word) { + { + let x = x; + } + for (let i = x; i; i = i) { + let j = i; + } + return j; + }", + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + let function = top_function(&db, module, "f"); + let body = function.body(&db).expect("body"); + let map = body_map(&db, module, body); + let events = ident_resolutions(&db, body, &map); + + let return_j = events + .iter() + .rev() + .find(|(name, _)| *name == "j") + .expect("return j"); + assert!(matches!(return_j.1, Resolution::Local(_))); +} + +#[test] +fn contract_fields_beat_top_level_functions_and_params_shadow_fields() { + let db = TestDb::default(); + let module = parse_module( + &db, + "function balance() returns (word) { return 0; } + contract C { + balance: word; + function f() returns (word) { return balance; } + function g(balance: word) returns (word) { return balance; } + }", + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + let field_function = contract_function(&db, module, "C", "f"); + let field_body = field_function.body(&db).expect("body"); + let field_map = body_map(&db, module, field_body); + let field_events = ident_resolutions(&db, field_body, &field_map); + assert!(matches!(field_events[0].1, Resolution::Field(_))); + + let param_function = contract_function(&db, module, "C", "g"); + let param_body = param_function.body(&db).expect("body"); + let param_map = body_map(&db, module, param_body); + let param_events = ident_resolutions(&db, param_body, ¶m_map); + assert!(matches!(param_events[0].1, Resolution::Param(_))); +} + +#[test] +fn duplicate_struct_fields_are_diagnosed_in_the_field_namespace() { + let db = TestDb::default(); + let module = parse_module( + &db, + "struct Pair { + value: word; + value: bool; + }", + ); + + let resolution = resolve_module(&db, module); + assert!(resolution.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + NameresDiagnostic::DuplicateDeclaration { + namespace: Namespace::Field, + name, + context: Some(context), + .. + } if name == "value" && context == "struct Pair" + ) + })); +} + +#[test] +fn unqualified_call_callee_prefers_contract_function_over_same_name_field() { + let db = TestDb::default(); + let module = parse_module( + &db, + "contract C { + balance: word; + function balance() returns (word) { return 7; } + function call() returns (word) { return balance(); } + function bare() returns (word) { return balance; } + }", + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + let call_function = contract_function(&db, module, "C", "call"); + let call_body = call_function.body(&db).expect("body"); + let call_map = body_map(&db, module, call_body); + let call_events = ident_resolutions(&db, call_body, &call_map); + let callee = call_events + .iter() + .find(|(name, _)| *name == "balance") + .expect("call callee"); + assert!(matches!( + callee.1, + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } + )); + + let bare_function = contract_function(&db, module, "C", "bare"); + let bare_body = bare_function.body(&db).expect("body"); + let bare_map = body_map(&db, module, bare_body); + let bare_events = ident_resolutions(&db, bare_body, &bare_map); + let bare = bare_events + .iter() + .find(|(name, _)| *name == "balance") + .expect("bare reference"); + assert!(matches!(bare.1, Resolution::Field(_))); +} + +#[test] +fn qualified_ctor_and_class_method_resolve_as_expected() { + let db = TestDb::default(); + let module = parse_module( + &db, + "enum Option { None, Some(word) } + enum Foo { Foo(word) } + trait Show { function show(x: self) returns (word); } + function good(x: word) returns (Option) { return Option.Some(x); } + function classCall(x: word) returns (word) { return Show.show(x); } + function qualified(x: word) returns (Option) { return Option.Some(x); } + function sameName(x: word) returns (Foo) { return Foo.Foo(x); }", + ); + let codes = diagnostic_codes(&db, module); + assert!(codes.is_empty()); + + let good = top_function(&db, module, "good"); + let good_body = good.body(&db).expect("body"); + let good_map = body_map(&db, module, good_body); + assert!(good_map.exprs.iter().any( + |entry| entry.body == good_body && matches!(entry.resolution, Resolution::Ctor { .. }) + )); + + let class_call = top_function(&db, module, "classCall"); + let class_body = class_call.body(&db).expect("body"); + let class_map = body_map(&db, module, class_body); + assert!(class_map.exprs.iter().any(|entry| entry.body == class_body + && matches!(entry.resolution, Resolution::ClassMethod { .. }))); + + let qualified = top_function(&db, module, "qualified"); + let qualified_body = qualified.body(&db).expect("body"); + let qualified_map = body_map(&db, module, qualified_body); + assert!( + qualified_map + .exprs + .iter() + .any(|entry| entry.body == qualified_body + && matches!(entry.resolution, Resolution::Ctor { .. })) + ); +} + +#[test] +fn self_qualified_contract_methods_do_not_shadow_same_named_local_adt_constructors() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract Option { + enum Option { None, Some(a) } + + function some(x: word) returns (Option) { + return Option.Some(x); + } + + function none() returns (Option) { + return Option.None; + } + + function read(o: Option) returns (word) { + match (o) { case Option.Some(x) { return x; } case Option.None { return 0; } } + } +} +"#, + ); + assert!(diagnostic_codes(&db, module).is_empty()); + + for name in ["some", "none"] { + let function = contract_function(&db, module, "Option", name); + let body = function.body(&db).expect("body"); + let map = body_map(&db, module, body); + assert!( + map.exprs.iter().any(|entry| { + entry.body == body && matches!(entry.resolution, Resolution::Ctor { .. }) + }), + "Option.{name} should resolve its qualified constructor expression" + ); + } + + let read = contract_function(&db, module, "Option", "read"); + let read_body = read.body(&db).expect("body"); + let read_map = body_map(&db, module, read_body); + assert_eq!( + read_map + .pats + .iter() + .filter(|entry| { + entry.body == read_body && matches!(entry.resolution, Resolution::Ctor { .. }) + }) + .count(), + 2 + ); +} + +#[test] +fn library_members_are_available_through_the_library_name() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +library Helpers { + alias Fixed = word[3]; + function id(x: Fixed) internal returns (Fixed) { return x; } +} + +function reuse(x: Helpers.Fixed) returns (Helpers.Fixed) { + return Helpers.id(x); +} +"#, + ); + + assert!(diagnostic_codes(&db, module).is_empty()); + let reuse = top_function(&db, module, "reuse"); + let body = reuse.body(&db).expect("body"); + let map = body_map(&db, module, body); + assert!(map.exprs.iter().any(|entry| { + entry.body == body + && matches!( + entry.resolution, + Resolution::Def { + kind: DefResolutionKind::Function, + .. + } + ) + })); +} + +#[test] +fn private_library_functions_stay_inside_the_library_scope() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +library Helpers { + function secret() private pure returns (word) { return 7; } + function reveal() internal pure returns (word) { return secret(); } +} + +function good() returns (word) { return Helpers.reveal(); } +function bad() returns (word) { return Helpers.secret(); } +"#, + ); + + let diagnostics = diagnostics(&db, module); + assert_eq!( + diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.as_deref() == Some("SC0101")) + .count(), + 1, + "{diagnostics:#?}" + ); + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains("undefined name: secret")), + "{diagnostics:#?}" + ); +} + +#[test] +fn external_contract_functions_are_not_bare_internal_calls() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +contract C { + function public_value() public pure returns (word) { return 1; } + function external_value() external pure returns (word) { return 2; } + function caller() returns (word) { + let value = public_value(); + return external_value(); + } +} +"#, + ); + + let diagnostics = diagnostics(&db, module); + assert_eq!( + diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.as_deref() == Some("SC0101")) + .count(), + 1, + "{diagnostics:#?}" + ); + assert!( + diagnostics.iter().any(|diagnostic| diagnostic + .message + .contains("undefined name: external_value")), + "{diagnostics:#?}" + ); +} + +#[test] +fn external_library_functions_cannot_call_themselves_through_the_library_name() { + let db = TestDb::default(); + let module = parse_module( + &db, + r#" +library Helpers { + function external_value() external pure returns (word) { return 2; } + function caller() internal pure returns (word) { + return Helpers.external_value(); + } +} +"#, + ); + + let diagnostics = diagnostics(&db, module); + assert_eq!( + diagnostics + .iter() + .filter(|diagnostic| diagnostic.code.as_deref() == Some("SC0101")) + .count(), + 1, + "{diagnostics:#?}" + ); +} + +#[test] +fn unqualified_same_name_constructor_is_rejected_with_unknown_wildcard_import() { + let db = TestDb::default(); + let module = parse_module( + &db, + "// migrate-syntax: keep-unqualified-constructor + enum Unit { Unit } + function make() returns (Unit) { return Unit; }", + ); + let function = top_function(&db, module, "make"); + let body = function.body(&db).expect("body"); + let scope = item_scope(&db, module); + let resolution = resolve_module_with_imports_and_policy( + &db, + module, + scope, + &UnknownWildcardImports, + NameresDiagnosticPolicy::Emit, + ); + let body_map = resolution + .bodies + .iter() + .find(|map| map.exprs.iter().any(|entry| entry.body == body)) + .expect("body map"); + let events = ident_resolutions(&db, body, body_map); + + assert!( + events + .iter() + .any(|(name, resolution)| *name == "Unit" && matches!(resolution, Resolution::Err)), + "unqualified same-name constructor should be rejected: {events:#?}" + ); + assert!( + resolution.diagnostics.iter().any(|diagnostic| matches!( + diagnostic, + NameresDiagnostic::UnqualifiedConstructor { + name, + qualification: Some(qualification), + .. + } if name == "Unit" && qualification == "Unit.Unit" + )), + "expected an actionable qualification diagnostic: {:#?}", + resolution.diagnostics + ); +} diff --git a/crates/parser/tests/properties.rs b/crates/parser/tests/properties.rs new file mode 100644 index 00000000..d4d79a3f --- /dev/null +++ b/crates/parser/tests/properties.rs @@ -0,0 +1,116 @@ +use hir::input::SourceFile; +use proptest::prelude::*; +use solcore_parser::{parse_diagnostics, parse_file_to_hir}; + +#[salsa::db] +#[derive(Default)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl solcore_parser::Db for TestDb {} + +const CORPUS_SEEDS: &[&str] = &[ + include_str!("fixtures/ok/no_diagnostics.solc"), + include_str!("fixtures/ok/contract_modifiers_constructor_fallback.solc"), + include_str!("fixtures/ok/match_arm_block.solc"), + include_str!("fixtures/corpus/fail/test/diagnostics/parse-error.solc"), +]; + +fn parse_without_large_test_stack(source: String) -> Vec { + let db = TestDb::default(); + let url = "memory:///property.solc".parse().expect("valid test URL"); + let file = SourceFile::new(&db, url, Some(source)); + let _ = parse_file_to_hir(&db, file).module(&db); + parse_diagnostics(&db, file) + .iter() + .map(|diagnostic| diagnostic.lower(&db).message.clone()) + .collect() +} + +fn mutated_corpus_source(seed: usize, position: usize, mutation: Vec) -> String { + let mut source = CORPUS_SEEDS[seed].chars().collect::>(); + let position = position % (source.len() + 1); + source.splice(position..position, mutation); + source.into_iter().collect() +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(128))] + + #[test] + fn arbitrary_utf8_source_never_panics( + source in prop::collection::vec(any::(), 0..512) + .prop_map(|characters| characters.into_iter().collect::()), + ) { + let _ = parse_without_large_test_stack(source); + } + + #[test] + fn mutations_of_existing_corpus_files_never_panic( + seed in 0..CORPUS_SEEDS.len(), + position in any::(), + mutation in prop::collection::vec(any::(), 0..128), + ) { + let _ = parse_without_large_test_stack(mutated_corpus_source(seed, position, mutation)); + } +} + +#[test] +fn right_nested_ternary_chain_uses_the_default_stack() { + let depth = 96; + let mut source = "function main() returns (word) { return ".to_owned(); + source.push_str(&"true ? 0 : ".repeat(depth)); + source.push_str("0; }"); + let diagnostics = parse_without_large_test_stack(source); + assert!( + diagnostics + .iter() + .any(|message| message.contains("expression nesting exceeds the compiler limit")), + "the conditional chain should be bounded before recursive parsing exhausts the default stack: {diagnostics:#?}" + ); +} + +#[test] +fn sequential_if_statements_do_not_count_as_nested_expressions() { + let mut source = "function main() returns (word) { ".to_owned(); + source.push_str(&"if (true) {} ".repeat(48)); + source.push_str("return 0; }"); + let diagnostics = parse_without_large_test_stack(source); + assert!( + diagnostics.is_empty(), + "sequential statements are not expression nesting: {diagnostics:#?}" + ); +} + +#[test] +fn standard_library_expressions_use_bounded_stack() { + let source = include_str!("../../../std/std.solc").to_owned(); + let diagnostics = std::thread::Builder::new() + .name("standard-library-parser".to_owned()) + // The unboxed precedence chain overflows at 1 MiB while the boxed + // parser has enough headroom to keep this stable across test hosts. + .stack_size(1024 * 1024) + .spawn(move || parse_without_large_test_stack(source)) + .expect("spawn parser thread") + .join() + .expect("parser thread should not overflow its stack"); + assert!( + diagnostics.is_empty(), + "standard-library expressions should parse on a bounded stack: {diagnostics:#?}" + ); +} diff --git a/crates/sonatina/Cargo.toml b/crates/sonatina/Cargo.toml new file mode 100644 index 00000000..447ce223 --- /dev/null +++ b/crates/sonatina/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "solcore-sonatina" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +hir = { workspace = true } +hull = { path = "../hull", package = "solcore-hull" } +smallvec = { workspace = true } +sonatina-ir = { workspace = true } +sonatina-triple = { workspace = true } +sonatina-verifier = { workspace = true } + +[dev-dependencies] +dir-test = "0.4.1" +hir-ty = { workspace = true } +insta = "1.43.2" +nameres = { workspace = true } +parser = { workspace = true } +salsa = { workspace = true } +solcore-test-utils = { path = "../test-utils" } +sonatina-codegen = { workspace = true } +specialize = { path = "../specialize", package = "solcore-specialize" } diff --git a/crates/sonatina/src/lib.rs b/crates/sonatina/src/lib.rs new file mode 100644 index 00000000..bf83d51a --- /dev/null +++ b/crates/sonatina/src/lib.rs @@ -0,0 +1,52 @@ +//! Checked Hull-to-Sonatina lowering for the EVM Osaka target. + +mod lower; + +use std::{error::Error, fmt}; + +use hir::Db as HirDb; +use hull::Program as HullProgram; +use sonatina_ir::{Module, ir_writer::ModuleWriter}; + +/// A failure while translating typed Hull into Sonatina IR. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TranslationError { + message: String, +} + +impl TranslationError { + pub(crate) fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for TranslationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl Error for TranslationError {} + +/// Lowers a checked Hull program to verified Sonatina IR. +pub fn translate_hull_program<'db>( + db: &'db dyn HirDb, + program: &HullProgram<'db>, +) -> Result { + lower::translate_hull_program(db, program) +} + +/// Lowers and prints a checked Hull program as textual Sonatina IR. +pub fn render_hull_program<'db>( + db: &'db dyn HirDb, + program: &HullProgram<'db>, +) -> Result { + let module = translate_hull_program(db, program)?; + Ok(ModuleWriter::new(&module).dump_string()) +} diff --git a/crates/sonatina/src/lower.rs b/crates/sonatina/src/lower.rs new file mode 100644 index 00000000..9fb46d96 --- /dev/null +++ b/crates/sonatina/src/lower.rs @@ -0,0 +1,2470 @@ +use std::collections::HashMap; + +use hir::{ + Db as HirDb, + ast::function::{YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind}, +}; +use hull::{ + Con, Expr as HullExpr, ExprKind, Function as HullFunction, Object as HullObject, PatKind, + Program as HullProgram, Stmt as HullStmt, StmtKind, Ty as HullTy, TyKind, +}; +use smallvec::{SmallVec, smallvec}; +use sonatina_ir::{ + BlockId, EmbedSymbol, I256, Immediate, Linkage, Module, Signature, Type, ValueId, + builder::{FunctionBuilder, ModuleBuilder, ObjectBuilder, Variable}, + func_cursor::InstInserter, + inst::{ + arith::{Add, Mul, Sar, Shl, Shr, Sub}, + cast::{Trunc, Zext}, + cmp::{Eq, Gt, IsZero, Lt, Sgt, Slt}, + control_flow::{Br, Call, Jump, Return, Unreachable}, + data::{ + EnumAssertVariant, EnumExtract, EnumIsVariant, EnumMake, ExtractValue, InsertValue, + SymAddr, SymSize, SymbolRef, + }, + evm::{ + EvmAddMod, EvmAddress, EvmBalance, EvmBaseFee, EvmBlobBaseFee, EvmBlobHash, + EvmBlockHash, EvmByte, EvmCall, EvmCallCode, EvmCallValue, EvmCalldataCopy, + EvmCalldataLoad, EvmCalldataSize, EvmCaller, EvmChainId, EvmClz, EvmCodeCopy, + EvmCodeSize, EvmCoinBase, EvmCreate, EvmCreate2, EvmDelegateCall, EvmExp, + EvmExtCodeCopy, EvmExtCodeHash, EvmExtCodeSize, EvmGas, EvmGasLimit, EvmGasPrice, + EvmInvalid, EvmKeccak256, EvmLog0, EvmLog1, EvmLog2, EvmLog3, EvmLog4, EvmMcopy, + EvmMload, EvmMsize, EvmMstore, EvmMstore8, EvmMulMod, EvmNumber, EvmOrigin, + EvmPrevRandao, EvmReturn, EvmReturnDataCopy, EvmReturnDataSize, EvmRevert, EvmSdiv, + EvmSelfBalance, EvmSelfDestruct, EvmSignExtend, EvmSload, EvmSmod, EvmSstore, + EvmStaticCall, EvmStop, EvmTimestamp, EvmTload, EvmTstore, EvmUdiv, EvmUmod, + inst_set::EvmInstSet, + }, + logic::{And, Not, Or, Xor}, + }, + isa::Isa, + isa::evm::Evm, + module::{FuncRef, ModuleCtx}, + types::{CompoundType, EnumReprHint, EnumVariantRef, VariantData}, +}; +use sonatina_triple::{Architecture, EvmVersion, OperatingSystem, TargetTriple, Vendor}; +use sonatina_verifier::{VerificationLevel, VerifierConfig, verify_module}; + +use crate::TranslationError; + +pub(super) fn translate_hull_program<'db>( + db: &'db dyn HirDb, + program: &HullProgram<'db>, +) -> Result { + Translator::new(db).translate(program) +} + +fn evm_isa() -> Evm { + Evm::new(TargetTriple::new( + Architecture::Evm, + Vendor::Ethereum, + OperatingSystem::Evm(EvmVersion::Osaka), + )) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum TyKey { + Word, + Bool, + Unit, + Product(Box, Box), + Sum(Box, Box), + Named(String, Box), + NamedRef(String), + Function(Vec, Box), +} + +impl TyKey { + fn of(ty: &HullTy<'_>) -> Self { + match &ty.kind { + TyKind::Word => Self::Word, + TyKind::Bool => Self::Bool, + TyKind::Unit => Self::Unit, + TyKind::Product(lhs, rhs) => { + Self::Product(Box::new(Self::of(lhs)), Box::new(Self::of(rhs))) + } + TyKind::Sum(lhs, rhs) => Self::Sum(Box::new(Self::of(lhs)), Box::new(Self::of(rhs))), + TyKind::Named { name, inner } => { + Self::Named(name.as_str().to_owned(), Box::new(Self::of(inner))) + } + TyKind::NamedRef { name } => Self::NamedRef(name.as_str().to_owned()), + TyKind::Function { params, ret } => Self::Function( + params.iter().map(Self::of).collect(), + Box::new(Self::of(ret)), + ), + } + } +} + +struct Translator<'db> { + db: &'db dyn HirDb, + builder: ModuleBuilder, + isa: Evm, + types: HashMap, + type_names: HashMap, + functions: HashMap, + function_returns: HashMap>, + entries: HashMap, + section_objects: HashMap, + next_type: usize, +} + +impl<'db> Translator<'db> { + fn new(db: &'db dyn HirDb) -> Self { + let isa = evm_isa(); + let builder = ModuleBuilder::new(ModuleCtx::new(&isa)); + Self { + db, + builder, + isa, + types: HashMap::new(), + type_names: HashMap::new(), + functions: HashMap::new(), + function_returns: HashMap::new(), + entries: HashMap::new(), + section_objects: HashMap::new(), + next_type: 0, + } + } + + fn inst_set(&self) -> &'static EvmInstSet { + self.isa.inst_set() + } + + fn translate(mut self, program: &HullProgram<'db>) -> Result { + if program.objects.is_empty() { + self.section_objects + .insert("root.runtime".to_owned(), "Output".to_owned()); + self.declare_code("root.runtime", &program.functions, &[], program.span)?; + self.lower_code("root.runtime", &program.functions, &[], program.span)?; + let entry = self.entry("root.runtime")?; + let mut object = ObjectBuilder::new("OutputDeploy"); + object.section("init").entry(entry); + object.section("runtime").entry(entry); + object + .declare(&mut self.builder) + .map_err(|err| TranslationError::new(format!("failed to declare object: {err}")))?; + } else { + for (index, object) in program.objects.iter().enumerate() { + self.declare_object_code(object, &format!("object{index}"))?; + } + for (index, object) in program.objects.iter().enumerate() { + self.lower_object_code(object, &format!("object{index}"))?; + } + for (index, object) in program.objects.iter().enumerate() { + self.declare_object(object, &format!("object{index}"))?; + } + } + + let module = self.builder.build(); + let report = verify_module(&module, &VerifierConfig::for_level(VerificationLevel::Full)); + if report.has_errors() { + return Err(TranslationError::new(format!( + "Sonatina verification failed:\n{report}" + ))); + } + Ok(module) + } + + fn declare_object_code( + &mut self, + object: &HullObject<'db>, + scope: &str, + ) -> Result<(), TranslationError> { + self.section_objects + .insert(format!("{scope}.init"), object.name.as_str().to_owned()); + self.declare_code( + &format!("{scope}.init"), + &object.code.functions, + &object.code.stmts, + object.code.span, + )?; + for (index, inner) in object.inners.iter().enumerate() { + self.declare_object_code(inner, &format!("{scope}.inner{index}"))?; + } + Ok(()) + } + + fn lower_object_code( + &mut self, + object: &HullObject<'db>, + scope: &str, + ) -> Result<(), TranslationError> { + self.lower_code( + &format!("{scope}.init"), + &object.code.functions, + &object.code.stmts, + object.code.span, + )?; + for (index, inner) in object.inners.iter().enumerate() { + self.lower_object_code(inner, &format!("{scope}.inner{index}"))?; + } + Ok(()) + } + + fn declare_object( + &mut self, + object: &HullObject<'db>, + scope: &str, + ) -> Result<(), TranslationError> { + let init = self.entry(&format!("{scope}.init"))?; + let mut builder = ObjectBuilder::new(object.name.as_str()); + builder.section("init").entry(init); + if object.inners.is_empty() { + builder.section("runtime").entry(init); + } else { + for (index, inner) in object.inners.iter().enumerate() { + let inner_scope = format!("{scope}.inner{index}"); + let section = if index == 0 { + "runtime".to_owned() + } else { + object_section_name(&inner_scope) + }; + self.declare_inner_object_sections(&mut builder, inner, &inner_scope, §ion)?; + builder + .section("init") + .embed_local(section, inner.name.as_str()); + } + } + builder + .declare(&mut self.builder) + .map_err(|err| TranslationError::new(format!("failed to declare object: {err}"))) + } + + fn declare_inner_object_sections( + &self, + builder: &mut ObjectBuilder, + object: &HullObject<'db>, + scope: &str, + section: &str, + ) -> Result<(), TranslationError> { + let entry = self.entry(&format!("{scope}.init"))?; + builder.section(section).entry(entry); + for (index, inner) in object.inners.iter().enumerate() { + let inner_scope = format!("{scope}.inner{index}"); + let inner_section = object_section_name(&inner_scope); + self.declare_inner_object_sections(builder, inner, &inner_scope, &inner_section)?; + builder + .section(section) + .embed_local(inner_section, inner.name.as_str()); + } + Ok(()) + } + + fn declare_code( + &mut self, + scope: &str, + functions: &[HullFunction<'db>], + _stmts: &[hull::Stmt<'db>], + _span: hir::span::Span<'db>, + ) -> Result<(), TranslationError> { + for function in functions { + let args = function + .args + .iter() + .map(|arg| self.lower_ty(&arg.ty)) + .collect::, _>>()?; + let ret = self.lower_ty(&function.ret)?; + let symbol = function_symbol(scope, function.name.as_str()); + let signature = if ret == Type::Unit { + Signature::new_unit(&symbol, Linkage::Private, &args) + } else { + Signature::new_single(&symbol, Linkage::Private, &args, ret) + }; + let func = self.builder.declare_function(signature).map_err(|err| { + TranslationError::new(format!("failed to declare `{symbol}`: {err}")) + })?; + self.functions + .insert(key(scope, function.name.as_str()), func); + self.function_returns + .insert(key(scope, function.name.as_str()), function.ret.clone()); + } + let entry_symbol = entry_symbol(scope); + let entry = self + .builder + .declare_function(Signature::new_unit(&entry_symbol, Linkage::Public, &[])) + .map_err(|err| TranslationError::new(format!("failed to declare entry: {err}")))?; + self.entries.insert(scope.to_owned(), entry); + Ok(()) + } + + fn lower_code( + &mut self, + scope: &str, + functions: &[HullFunction<'db>], + stmts: &[hull::Stmt<'db>], + span: hir::span::Span<'db>, + ) -> Result<(), TranslationError> { + for function in functions { + self.lower_function(scope, function)?; + } + self.lower_entry(scope, functions, stmts, span) + } + + fn entry(&self, scope: &str) -> Result { + self.entries + .get(scope) + .copied() + .ok_or_else(|| TranslationError::new(format!("missing section entry for `{scope}`"))) + } + + fn lower_ty(&mut self, ty: &HullTy<'db>) -> Result { + let key = TyKey::of(ty); + if let Some(existing) = self.types.get(&key) { + return Ok(*existing); + } + let lowered = match &ty.kind { + TyKind::Word => Type::I256, + TyKind::Bool => Type::I1, + TyKind::Unit => Type::Unit, + TyKind::Product(lhs, rhs) => { + let lhs = self.lower_ty(lhs)?; + let rhs = self.lower_ty(rhs)?; + let name = self.fresh_type_name("product", &key); + self.builder.declare_struct_type(&name, &[lhs, rhs], false) + } + TyKind::Sum(lhs, rhs) if is_unit_ty(lhs) && is_unit_ty(rhs) => Type::I1, + TyKind::Sum(lhs, rhs) => { + let lhs_ty = self.lower_ty(lhs)?; + let rhs_ty = self.lower_ty(rhs)?; + let name = self.fresh_type_name("sum", &key); + let variants = [ + VariantData { + name: "inl".to_owned(), + explicit_discriminant: None, + fields: (!is_unit_ty(lhs)).then_some(lhs_ty).into_iter().collect(), + }, + VariantData { + name: "inr".to_owned(), + explicit_discriminant: None, + fields: (!is_unit_ty(rhs)).then_some(rhs_ty).into_iter().collect(), + }, + ]; + self.builder + .declare_enum_type(&name, &variants, EnumReprHint::Default) + } + // Hull named types are transparent: their expressions are built from the + // same product/sum values as the representation type. Reusing that + // structural type avoids introducing a nominal Sonatina mismatch. + TyKind::Named { inner, .. } => self.lower_ty(inner)?, + TyKind::NamedRef { .. } => Type::I256, + TyKind::Function { params, ret } => { + let params = params + .iter() + .map(|param| self.lower_ty(param)) + .collect::, _>>()?; + let ret = self.lower_ty(ret)?; + let returns = (ret != Type::Unit) + .then_some(ret) + .into_iter() + .collect::>(); + self.builder.declare_func_type(¶ms, &returns) + } + }; + self.types.insert(key, lowered); + Ok(lowered) + } + + fn fresh_type_name(&mut self, preferred: &str, key: &TyKey) -> String { + if let Some(name) = self.type_names.get(key) { + return name.clone(); + } + let stem = sanitize(preferred); + let name = format!("solcore_{stem}_{}", self.next_type); + self.next_type += 1; + self.type_names.insert(key.clone(), name.clone()); + name + } + + fn lower_function( + &mut self, + scope: &str, + function: &HullFunction<'db>, + ) -> Result<(), TranslationError> { + let func_ref = self + .functions + .get(&key(scope, function.name.as_str())) + .copied() + .ok_or_else(|| { + TranslationError::new(format!( + "missing declaration for `{}`", + function.name.as_str() + )) + })?; + let mut lowerer = FunctionLowerer::new(self, scope, func_ref, function.ret.clone()); + for (index, arg) in function.args.iter().enumerate() { + let value = lowerer.fb.func.arg_values[index]; + lowerer.bind_parameter(arg.name.as_str(), &arg.ty, value)?; + } + let terminated = lowerer.lower_stmts(&function.body)?; + lowerer.finish(terminated) + } + + fn lower_entry( + &mut self, + scope: &str, + functions: &[HullFunction<'db>], + stmts: &[hull::Stmt<'db>], + span: hir::span::Span<'db>, + ) -> Result<(), TranslationError> { + let func_ref = self.entry(scope)?; + let unit = HullTy::unit(span); + let mut lowerer = FunctionLowerer::new(self, scope, func_ref, unit); + let mut terminated = lowerer.lower_stmts(stmts)?; + if !terminated + && stmts.is_empty() + && let Some(main) = functions.iter().find(|function| { + function.args.is_empty() + && (function.name.as_str() == "main" + || function.name.as_str().starts_with("main_") + || function.name.as_str().contains("_main_")) + }) + { + let callee = lowerer + .module + .functions + .get(&key(scope, main.name.as_str())) + .copied() + .ok_or_else(|| TranslationError::new("missing main declaration"))?; + let ret = lowerer.module.lower_ty(&main.ret)?; + let call = Call::new(lowerer.module.inst_set(), callee, SmallVec::new()); + if ret == Type::Unit { + lowerer.fb.insert_inst_no_result(call); + } else { + let result = lowerer.fb.insert_inst(call, ret); + if ret.is_integral() { + let result = lowerer.coerce(result, Type::I256)?; + let zero = lowerer.fb.make_imm_value(I256::zero()); + let size = lowerer.fb.make_imm_value(I256::from(32u8)); + lowerer.fb.insert_inst_no_result(EvmMstore::new( + lowerer.module.inst_set(), + zero, + result, + )); + lowerer.fb.insert_inst_no_result(EvmReturn::new( + lowerer.module.inst_set(), + zero, + size, + )); + terminated = true; + } + } + } + lowerer.finish(terminated) + } +} + +#[derive(Clone)] +struct Binding<'db> { + var: Variable, + ty: Type, + hull_ty: HullTy<'db>, +} + +enum BuiltinOutcome { + Value(ValueId), + Unit, + Terminated, +} + +struct FunctionLowerer<'a, 'db> { + module: &'a mut Translator<'db>, + scope: String, + fb: FunctionBuilder, + scopes: Vec>>, + ret: HullTy<'db>, + break_targets: Vec, + continue_targets: Vec, +} + +impl<'a, 'db> FunctionLowerer<'a, 'db> { + fn new( + module: &'a mut Translator<'db>, + scope: &str, + func_ref: FuncRef, + ret: HullTy<'db>, + ) -> Self { + let mut fb = module.builder.func_builder::(func_ref); + let entry = fb.append_block(); + fb.switch_to_block(entry); + Self { + module, + scope: scope.to_owned(), + fb, + scopes: vec![HashMap::new()], + ret, + break_targets: Vec::new(), + continue_targets: Vec::new(), + } + } + + fn finish(mut self, terminated: bool) -> Result<(), TranslationError> { + if !terminated { + let ret = self.module.lower_ty(&self.ret)?; + if ret == Type::Unit { + self.fb + .insert_inst_no_result(Return::new_unit(self.module.inst_set())); + } else { + let value = zero_for_type(&mut self.fb, self.module.inst_set(), ret); + self.fb + .insert_inst_no_result(Return::new_single(self.module.inst_set(), value)); + } + } + self.fb.seal_all(); + self.fb.finish(); + Ok(()) + } + + fn bind_parameter( + &mut self, + name: &str, + hull_ty: &HullTy<'db>, + value: ValueId, + ) -> Result<(), TranslationError> { + let ty = self.module.lower_ty(hull_ty)?; + let var = self.fb.declare_var(ty); + self.fb.def_var(var, value); + self.insert_binding( + name, + Binding { + var, + ty, + hull_ty: hull_ty.clone(), + }, + ); + Ok(()) + } + + fn declare_binding( + &mut self, + name: &str, + hull_ty: &HullTy<'db>, + ) -> Result, TranslationError> { + let ty = self.module.lower_ty(hull_ty)?; + let var = self.fb.declare_var(ty); + let initial = zero_for_type(&mut self.fb, self.module.inst_set(), ty); + self.fb.def_var(var, initial); + let binding = Binding { + var, + ty, + hull_ty: hull_ty.clone(), + }; + self.insert_binding(name, binding.clone()); + Ok(binding) + } + + fn bind_value( + &mut self, + name: &str, + hull_ty: &HullTy<'db>, + value: ValueId, + ) -> Result<(), TranslationError> { + let binding = self.declare_binding(name, hull_ty)?; + let value = self.coerce(value, binding.ty)?; + self.fb.def_var(binding.var, value); + Ok(()) + } + + fn insert_binding(&mut self, name: &str, binding: Binding<'db>) { + self.scopes + .last_mut() + .expect("scope stack is never empty") + .insert(name.to_owned(), binding); + } + + fn lookup(&self, name: &str) -> Result, TranslationError> { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + .ok_or_else(|| TranslationError::new(format!("undefined Hull variable `{name}`"))) + } + + fn push_scope(&mut self) { + self.scopes.push(HashMap::new()); + } + + fn pop_scope(&mut self) { + self.scopes.pop(); + } + + fn lower_stmts(&mut self, stmts: &[HullStmt<'db>]) -> Result { + for stmt in stmts { + if self.lower_stmt(stmt)? { + return Ok(true); + } + } + Ok(false) + } + + fn lower_stmt(&mut self, stmt: &HullStmt<'db>) -> Result { + match &stmt.kind { + StmtKind::Let { name, ty } => { + self.declare_binding(name.as_str(), ty)?; + Ok(false) + } + StmtKind::Assign { lhs, rhs } => { + let value = self.lower_expr(rhs)?; + self.assign(lhs, value)?; + Ok(false) + } + StmtKind::Expr(expr) => { + if let ExprKind::Call { callee, args } = &expr.kind + && !self + .module + .functions + .contains_key(&key(&self.scope, callee.as_str())) + && is_terminal_evm_builtin(callee.as_str()) + { + let values = args + .iter() + .map(|arg| self.lower_expr(arg)) + .collect::, _>>()?; + return Ok(matches!( + self.lower_evm_builtin(callee.as_str(), &values)?, + BuiltinOutcome::Terminated + )); + } + let _ = self.lower_expr(expr)?; + Ok(false) + } + StmtKind::Return(expr) => { + let value = self.lower_expr(expr)?; + let ret_ty = self.module.lower_ty(&self.ret)?; + if ret_ty == Type::Unit { + self.fb + .insert_inst_no_result(Return::new_unit(self.module.inst_set())); + } else { + let value = self.coerce(value, ret_ty)?; + self.fb + .insert_inst_no_result(Return::new_single(self.module.inst_set(), value)); + } + Ok(true) + } + StmtKind::Block(body) => { + self.push_scope(); + let terminated = self.lower_stmts(body)?; + self.pop_scope(); + Ok(terminated) + } + StmtKind::For { + init, + cond, + post, + body, + } => self.lower_for(init, cond, post, body), + StmtKind::Break => { + let target = self + .break_targets + .last() + .copied() + .ok_or_else(|| TranslationError::new("break outside loop"))?; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), target)); + Ok(true) + } + StmtKind::Continue => { + let target = self + .continue_targets + .last() + .copied() + .ok_or_else(|| TranslationError::new("continue outside loop"))?; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), target)); + Ok(true) + } + StmtKind::Match { + target, + scrutinee, + alts, + } => self.lower_match(target, scrutinee, alts), + StmtKind::Assembly(stmts) => self.lower_yul_stmts(stmts), + StmtKind::Revert(_) => { + let zero = self.fb.make_imm_value(I256::zero()); + self.fb + .insert_inst_no_result(EvmRevert::new(self.module.inst_set(), zero, zero)); + Ok(true) + } + StmtKind::Comment(_) => Ok(false), + } + } + + fn lower_expr(&mut self, expr: &HullExpr<'db>) -> Result { + match &expr.kind { + ExprKind::Word(value) => self.word_value(value), + ExprKind::Bool(value) => Ok(self.fb.make_imm_value(*value)), + ExprKind::Unit => Ok(self.fb.make_undef_value(Type::Unit)), + ExprKind::Var(name) => { + let binding = self.lookup(name.as_str())?; + Ok(self.fb.use_var(binding.var)) + } + ExprKind::Pair(lhs, rhs) => { + let ty = self.module.lower_ty(&expr.ty)?; + let (lhs_ty, rhs_ty) = product_parts(&expr.ty)?; + let lhs = self.lower_expr(lhs)?; + let lhs_target = self.module.lower_ty(lhs_ty)?; + let lhs = self.coerce(lhs, lhs_target)?; + let rhs = self.lower_expr(rhs)?; + let rhs_target = self.module.lower_ty(rhs_ty)?; + let rhs = self.coerce(rhs, rhs_target)?; + let mut value = self.fb.make_undef_value(ty); + let zero = self.index_value(0); + value = self.fb.insert_inst( + InsertValue::new(self.module.inst_set(), value, zero, lhs), + ty, + ); + let one = self.index_value(1); + Ok(self.fb.insert_inst( + InsertValue::new(self.module.inst_set(), value, one, rhs), + ty, + )) + } + ExprKind::Fst(inner) => { + let value = self.lower_expr(inner)?; + let (lhs, _) = product_parts(&inner.ty)?; + let ty = self.module.lower_ty(lhs)?; + let index = self.index_value(0); + Ok(self + .fb + .insert_inst(ExtractValue::new(self.module.inst_set(), value, index), ty)) + } + ExprKind::Snd(inner) => { + let value = self.lower_expr(inner)?; + let (_, rhs) = product_parts(&inner.ty)?; + let ty = self.module.lower_ty(rhs)?; + let index = self.index_value(1); + Ok(self + .fb + .insert_inst(ExtractValue::new(self.module.inst_set(), value, index), ty)) + } + ExprKind::Inl { target, value } => { + let value = self.lower_expr(value)?; + self.lower_variant(target, 0, value) + } + ExprKind::Inr { target, value } => { + let value = self.lower_expr(value)?; + self.lower_variant(target, 1, value) + } + ExprKind::InK { + index, + target, + value, + } => { + let value = self.lower_expr(value)?; + self.lower_injection(target, *index, value) + } + ExprKind::Call { callee, args } => { + let values = args + .iter() + .map(|arg| self.lower_expr(arg)) + .collect::, _>>()?; + if let Some(func) = self + .module + .functions + .get(&key(&self.scope, callee.as_str())) + .copied() + { + let ret = self.module.lower_ty(&expr.ty)?; + let call = Call::new(self.module.inst_set(), func, values); + if ret == Type::Unit { + self.fb.insert_inst_no_result(call); + Ok(self.fb.make_undef_value(Type::Unit)) + } else { + Ok(self.fb.insert_inst(call, ret)) + } + } else { + self.lower_builtin_call(callee.as_str(), &values, &expr.ty) + } + } + ExprKind::If { + cond, + then_expr, + else_expr, + .. + } => self.lower_if_expr(cond, then_expr, else_expr, &expr.ty), + } + } + + fn assign(&mut self, lhs: &HullExpr<'db>, value: ValueId) -> Result<(), TranslationError> { + let (name, path) = assignment_path(lhs)?; + let binding = self.lookup(name)?; + let value = if path.is_empty() { + self.coerce(value, binding.ty)? + } else { + let root = self.fb.use_var(binding.var); + self.insert_at_path(root, &binding.hull_ty, &path, value)? + }; + self.fb.def_var(binding.var, value); + Ok(()) + } + + fn insert_at_path( + &mut self, + aggregate: ValueId, + aggregate_ty: &HullTy<'db>, + path: &[usize], + value: ValueId, + ) -> Result { + let Some((&head, tail)) = path.split_first() else { + let target = self.module.lower_ty(aggregate_ty)?; + return self.coerce(value, target); + }; + let (lhs, rhs) = product_parts(aggregate_ty)?; + let field_ty = if head == 0 { lhs } else { rhs }; + let field = if tail.is_empty() { + let target = self.module.lower_ty(field_ty)?; + self.coerce(value, target)? + } else { + let index = self.index_value(head); + let extracted = self.fb.insert_inst( + ExtractValue::new(self.module.inst_set(), aggregate, index), + self.module.lower_ty(field_ty)?, + ); + self.insert_at_path(extracted, field_ty, tail, value)? + }; + let index = self.index_value(head); + Ok(self.fb.insert_inst( + InsertValue::new(self.module.inst_set(), aggregate, index, field), + self.module.lower_ty(aggregate_ty)?, + )) + } + + fn lower_if_expr( + &mut self, + cond: &HullExpr<'db>, + then_expr: &HullExpr<'db>, + else_expr: &HullExpr<'db>, + result_ty: &HullTy<'db>, + ) -> Result { + let ty = self.module.lower_ty(result_ty)?; + let result = self.fb.declare_var(ty); + let initial = zero_for_type(&mut self.fb, self.module.inst_set(), ty); + self.fb.def_var(result, initial); + let then_block = self.fb.append_block(); + let else_block = self.fb.append_block(); + let done = self.fb.append_block(); + let cond = self.lower_condition(cond)?; + self.fb.insert_inst_no_result(Br::new( + self.module.inst_set(), + cond, + then_block, + else_block, + )); + self.fb.switch_to_block(then_block); + let then_value = self.lower_expr(then_expr)?; + let then_value = self.coerce(then_value, ty)?; + self.fb.def_var(result, then_value); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + self.fb.switch_to_block(else_block); + let else_value = self.lower_expr(else_expr)?; + let else_value = self.coerce(else_value, ty)?; + self.fb.def_var(result, else_value); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + self.fb.switch_to_block(done); + Ok(self.fb.use_var(result)) + } + + fn lower_for( + &mut self, + init: &[HullStmt<'db>], + cond: &HullExpr<'db>, + post: &[HullStmt<'db>], + body: &[HullStmt<'db>], + ) -> Result { + self.push_scope(); + if self.lower_stmts(init)? { + self.pop_scope(); + return Ok(true); + } + let header = self.fb.append_block(); + let body_block = self.fb.append_block(); + let post_block = self.fb.append_block(); + let done = self.fb.append_block(); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), header)); + self.fb.switch_to_block(header); + let cond = self.lower_condition(cond)?; + self.fb + .insert_inst_no_result(Br::new(self.module.inst_set(), cond, body_block, done)); + self.fb.switch_to_block(body_block); + self.break_targets.push(done); + self.continue_targets.push(post_block); + let body_terminated = self.lower_stmts(body)?; + self.continue_targets.pop(); + self.break_targets.pop(); + if !body_terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), post_block)); + } + self.fb.switch_to_block(post_block); + let post_terminated = self.lower_stmts(post)?; + if !post_terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), header)); + } + self.fb.switch_to_block(done); + self.pop_scope(); + Ok(false) + } + + fn lower_match( + &mut self, + target: &HullTy<'db>, + scrutinee: &HullExpr<'db>, + alts: &[hull::Alt<'db>], + ) -> Result { + let value = self.lower_expr(scrutinee)?; + let done = self.fb.append_block(); + let mut has_fallthrough = false; + let mut exhaustive = false; + for alt in alts { + let arm = self.fb.append_block(); + let catch_all = matches!(&alt.pat.kind, PatKind::Wildcard | PatKind::Var(_)); + let next = (!catch_all).then(|| self.fb.append_block()); + if catch_all { + exhaustive = true; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), arm)); + } else { + match &alt.pat.kind { + PatKind::Wildcard | PatKind::Var(_) => unreachable!(), + pat => { + let cond = self.pattern_condition(value, target, pat)?; + self.fb.insert_inst_no_result(Br::new( + self.module.inst_set(), + cond, + arm, + next.expect("non-catch-all has a next block"), + )); + } + } + } + self.fb.switch_to_block(arm); + self.push_scope(); + let (binder_ty, binder_value) = self.pattern_payload(value, target, &alt.pat.kind)?; + self.bind_value(alt.binder.as_str(), &binder_ty, binder_value)?; + if let PatKind::Var(name) = &alt.pat.kind { + self.bind_value(name.as_str(), target, value)?; + } + let terminated = self.lower_stmts(&alt.body)?; + self.pop_scope(); + if !terminated { + has_fallthrough = true; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + if let Some(next) = next { + self.fb.switch_to_block(next); + } else { + break; + } + } + if !exhaustive { + has_fallthrough = true; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + self.fb.switch_to_block(done); + if has_fallthrough { + Ok(false) + } else { + self.fb + .insert_inst_no_result(Unreachable::new_unchecked(self.module.inst_set())); + Ok(true) + } + } + + fn pattern_condition( + &mut self, + value: ValueId, + target: &HullTy<'db>, + pat: &PatKind, + ) -> Result { + match pat { + PatKind::Con(Con::Inl) => self.is_variant(value, target, 0), + PatKind::Con(Con::Inr) => self.is_variant(value, target, 1), + PatKind::Con(Con::InK(index)) => self.in_k_condition(value, target, *index), + PatKind::IntLit(text) => { + let rhs = self.word_value(text)?; + Ok(self + .fb + .insert_inst(Eq::new(self.module.inst_set(), value, rhs), Type::I1)) + } + PatKind::Wildcard | PatKind::Var(_) => Ok(self.fb.make_imm_value(true)), + } + } + + fn pattern_payload( + &mut self, + value: ValueId, + target: &HullTy<'db>, + pat: &PatKind, + ) -> Result<(HullTy<'db>, ValueId), TranslationError> { + match pat { + PatKind::Con(Con::Inl) => self.extract_variant(value, target, 0), + PatKind::Con(Con::Inr) => self.extract_variant(value, target, 1), + PatKind::Con(Con::InK(index)) => self.extract_in_k(value, target, *index), + PatKind::Wildcard | PatKind::Var(_) | PatKind::IntLit(_) => Ok((target.clone(), value)), + } + } + + fn is_variant( + &mut self, + value: ValueId, + target: &HullTy<'db>, + index: u32, + ) -> Result { + if is_bool_like(target) { + let expected = self.fb.make_imm_value(index != 0); + return Ok(self + .fb + .insert_inst(Eq::new(self.module.inst_set(), value, expected), Type::I1)); + } + let variant = self.variant_ref(target, index)?; + Ok(self.fb.insert_inst( + EnumIsVariant::new(self.module.inst_set(), value, variant), + Type::I1, + )) + } + + fn extract_variant( + &mut self, + value: ValueId, + target: &HullTy<'db>, + index: u32, + ) -> Result<(HullTy<'db>, ValueId), TranslationError> { + let (lhs, rhs) = sum_parts(target)?; + let payload = if index == 0 { lhs } else { rhs }; + if is_bool_like(target) || is_unit_ty(payload) { + return Ok((payload.clone(), self.fb.make_undef_value(Type::Unit))); + } + let variant = self.variant_ref(target, index)?; + self.fb.insert_inst_no_result(EnumAssertVariant::new( + self.module.inst_set(), + value, + variant, + )); + let field = self.index_value(0); + let payload_ty = self.module.lower_ty(payload)?; + let value = self.fb.insert_inst( + EnumExtract::new(self.module.inst_set(), value, variant, field), + payload_ty, + ); + Ok((payload.clone(), value)) + } + + fn in_k_condition( + &mut self, + value: ValueId, + target: &HullTy<'db>, + index: usize, + ) -> Result { + if !matches!(target.strip_named().kind, TyKind::Sum(_, _)) { + return if index == 0 { + Ok(self.fb.make_imm_value(true)) + } else { + Err(TranslationError::new(format!( + "bad in({index}) pattern for non-sum Hull type" + ))) + }; + } + if index == 0 { + return self.is_variant(value, target, 0); + } + if is_bool_like(target) { + return if index == 1 { + self.is_variant(value, target, 1) + } else { + Err(TranslationError::new(format!( + "bad in({index}) pattern for boolean sum" + ))) + }; + } + + let result = self.fb.declare_var(Type::I1); + let initial = self.fb.make_imm_value(false); + self.fb.def_var(result, initial); + let right = self.fb.append_block(); + let not_right = self.fb.append_block(); + let done = self.fb.append_block(); + let is_right = self.is_variant(value, target, 1)?; + self.fb + .insert_inst_no_result(Br::new(self.module.inst_set(), is_right, right, not_right)); + self.fb.switch_to_block(right); + let (rhs, nested) = self.extract_variant(value, target, 1)?; + let nested_cond = self.in_k_condition(nested, &rhs, index - 1)?; + self.fb.def_var(result, nested_cond); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + self.fb.switch_to_block(not_right); + let false_value = self.fb.make_imm_value(false); + self.fb.def_var(result, false_value); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + self.fb.switch_to_block(done); + Ok(self.fb.use_var(result)) + } + + fn extract_in_k( + &mut self, + value: ValueId, + target: &HullTy<'db>, + index: usize, + ) -> Result<(HullTy<'db>, ValueId), TranslationError> { + if !matches!(target.strip_named().kind, TyKind::Sum(_, _)) { + return if index == 0 { + Ok((target.clone(), value)) + } else { + Err(TranslationError::new(format!( + "bad in({index}) payload for non-sum Hull type" + ))) + }; + } + if index == 0 { + return self.extract_variant(value, target, 0); + } + let (rhs, nested) = self.extract_variant(value, target, 1)?; + self.extract_in_k(nested, &rhs, index - 1) + } + + fn lower_injection( + &mut self, + target: &HullTy<'db>, + index: usize, + payload: ValueId, + ) -> Result { + if !matches!(target.strip_named().kind, TyKind::Sum(_, _)) { + if index != 0 { + return Err(TranslationError::new(format!( + "bad in({index}) injection for non-sum Hull type" + ))); + } + let target_ty = self.module.lower_ty(target)?; + return self.coerce(payload, target_ty); + } + if is_bool_like(target) { + return match index { + 0 => Ok(self.fb.make_imm_value(false)), + 1 => Ok(self.fb.make_imm_value(true)), + _ => Err(TranslationError::new(format!( + "bad in({index}) injection for boolean sum" + ))), + }; + } + let (_, rhs) = sum_parts(target)?; + if index == 0 { + self.lower_variant(target, 0, payload) + } else if index == 1 { + let nested = self.lower_injection(rhs, 0, payload)?; + self.lower_variant(target, 1, nested) + } else { + let nested = self.lower_injection(rhs, index - 1, payload)?; + self.lower_variant(target, 1, nested) + } + } + + fn lower_variant( + &mut self, + target: &HullTy<'db>, + variant_index: u32, + payload: ValueId, + ) -> Result { + if is_bool_like(target) { + return Ok(self.fb.make_imm_value(variant_index != 0)); + } + let (lhs, rhs) = sum_parts(target)?; + let payload_ty = if variant_index == 0 { lhs } else { rhs }; + let ty = self.module.lower_ty(target)?; + let variant = self.variant_ref(target, variant_index)?; + let values = if is_unit_ty(payload_ty) { + SmallVec::new() + } else { + let target = self.module.lower_ty(payload_ty)?; + smallvec![self.coerce(payload, target)?] + }; + Ok(self.fb.insert_inst( + EnumMake::new(self.module.inst_set(), ty, variant, values), + ty, + )) + } + + fn variant_ref( + &mut self, + target: &HullTy<'db>, + index: u32, + ) -> Result { + let ty = self.module.lower_ty(target)?; + let Type::Compound(enum_ty) = ty else { + return Err(TranslationError::new( + "sum did not lower to a Sonatina enum", + )); + }; + if !matches!( + ty.resolve_compound(&self.fb.module_builder.ctx), + Some(CompoundType::Enum(_)) + ) { + return Err(TranslationError::new("sum target is not a Sonatina enum")); + } + Ok(EnumVariantRef::new(enum_ty, index)) + } + + fn lower_condition(&mut self, expr: &HullExpr<'db>) -> Result { + let value = self.lower_expr(expr)?; + self.condition_value(value) + } + + fn condition_value(&mut self, value: ValueId) -> Result { + match self.fb.type_of(value) { + Type::I1 => Ok(value), + ty if ty.is_integral() => { + let zero = zero_for_type(&mut self.fb, self.module.inst_set(), ty); + let is_zero = self + .fb + .insert_inst(Eq::new(self.module.inst_set(), value, zero), Type::I1); + Ok(self + .fb + .insert_inst(IsZero::new(self.module.inst_set(), is_zero), Type::I1)) + } + ty => Err(TranslationError::new(format!( + "cannot use Sonatina type `{ty:?}` as a condition" + ))), + } + } + + fn coerce(&mut self, value: ValueId, target: Type) -> Result { + let source = self.fb.type_of(value); + if source == target { + return Ok(value); + } + if source.is_integral() && target.is_integral() { + if source < target { + return Ok(self + .fb + .insert_inst(Zext::new(self.module.inst_set(), value, target), target)); + } + return Ok(self + .fb + .insert_inst(Trunc::new(self.module.inst_set(), value, target), target)); + } + Err(TranslationError::new(format!( + "cannot coerce Sonatina value from `{source:?}` to `{target:?}`" + ))) + } + + fn index_value(&mut self, index: usize) -> ValueId { + self.fb.make_imm_value(I256::from(index)) + } + + fn word_value(&mut self, value: &str) -> Result { + let wrapped = + hull::wrap_word_literal(value).map_err(|err| TranslationError::new(err.to_string()))?; + let immediate = if let Some(hex) = wrapped + .strip_prefix("0x") + .or_else(|| wrapped.strip_prefix("0X")) + { + I256::from_be_bytes(&decode_hex_word(hex)?) + } else { + let unsigned = sonatina_ir::U256::from_dec_str(&wrapped).map_err(|err| { + TranslationError::new(format!("invalid 256-bit word literal `{value}`: {err}")) + })?; + I256::from_u256(unsigned) + }; + Ok(self.fb.make_imm_value(immediate)) + } + + fn lower_builtin_call( + &mut self, + name: &str, + args: &[ValueId], + result: &HullTy<'db>, + ) -> Result { + if is_primitive_name(name) { + return self.lower_primitive_call(name, args, result); + } + let result_ty = self.module.lower_ty(result)?; + match self.lower_evm_builtin(name, args)? { + BuiltinOutcome::Value(value) => self.coerce(value, result_ty), + BuiltinOutcome::Unit => Ok(self.fb.make_undef_value(Type::Unit)), + BuiltinOutcome::Terminated => Ok(self.fb.make_undef_value(Type::Unit)), + } + } + + fn lower_yul_stmts(&mut self, stmts: &[YulStmt<'db>]) -> Result { + self.push_scope(); + let terminated = self.lower_yul_stmt_seq(stmts)?; + self.pop_scope(); + Ok(terminated) + } + + fn lower_yul_stmt_seq(&mut self, stmts: &[YulStmt<'db>]) -> Result { + for stmt in stmts { + if self.lower_yul_stmt(stmt)? { + return Ok(true); + } + } + Ok(false) + } + + fn lower_yul_stmt(&mut self, stmt: &YulStmt<'db>) -> Result { + match &stmt.kind { + YulStmtKind::Block(stmts) => self.lower_yul_stmts(stmts), + YulStmtKind::Let { names, init } => { + let value = init + .as_ref() + .map(|expr| self.lower_yul_expr(expr)) + .transpose()?; + if names.len() > 1 && value.is_some() { + return Err(TranslationError::new( + "multi-result inline Yul let is not supported", + )); + } + for (index, name) in names.iter().enumerate() { + let name = self.yul_name(name); + let ty = HullTy::word(stmt.span); + let binding = self.declare_binding(&name, &ty)?; + if index == 0 + && let Some(value) = value + { + let value = self.coerce(value, Type::I256)?; + self.fb.def_var(binding.var, value); + } + } + Ok(false) + } + YulStmtKind::Assign { names, value } => { + if names.len() != 1 { + return Err(TranslationError::new( + "multi-result inline Yul assignment is not supported", + )); + } + let value = self.lower_yul_expr(value)?; + let name = self.yul_name(&names[0]); + let binding = self.lookup(&name)?; + let value = self.coerce(value, binding.ty)?; + self.fb.def_var(binding.var, value); + Ok(false) + } + YulStmtKind::Expr(expr) => { + if let YulExprKind::Call { name, args } = &expr.kind { + let name = self.yul_name(name); + let args = args + .iter() + .map(|arg| self.lower_yul_expr(arg)) + .collect::, _>>()?; + return match self.lower_evm_builtin(&name, &args)? { + BuiltinOutcome::Terminated => Ok(true), + BuiltinOutcome::Unit | BuiltinOutcome::Value(_) => Ok(false), + }; + } + let _ = self.lower_yul_expr(expr)?; + Ok(false) + } + YulStmtKind::If { cond, body } => { + let body_block = self.fb.append_block(); + let done = self.fb.append_block(); + let cond = self.lower_yul_expr(cond)?; + let cond = self.condition_value(cond)?; + self.fb.insert_inst_no_result(Br::new( + self.module.inst_set(), + cond, + body_block, + done, + )); + self.fb.switch_to_block(body_block); + let terminated = self.lower_yul_stmts(body)?; + if !terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + self.fb.switch_to_block(done); + Ok(false) + } + YulStmtKind::For { + init, + cond, + post, + body, + } => { + self.push_scope(); + // Yul loop-init bindings are visible to the condition, post, and + // body for the entire loop scope. + if self.lower_yul_stmt_seq(init)? { + self.pop_scope(); + return Ok(true); + } + let header = self.fb.append_block(); + let body_block = self.fb.append_block(); + let post_block = self.fb.append_block(); + let done = self.fb.append_block(); + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), header)); + self.fb.switch_to_block(header); + let cond = self.lower_yul_expr(cond)?; + let cond = self.condition_value(cond)?; + self.fb.insert_inst_no_result(Br::new( + self.module.inst_set(), + cond, + body_block, + done, + )); + self.fb.switch_to_block(body_block); + self.break_targets.push(done); + self.continue_targets.push(post_block); + let body_terminated = self.lower_yul_stmts(body)?; + self.continue_targets.pop(); + self.break_targets.pop(); + if !body_terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), post_block)); + } + self.fb.switch_to_block(post_block); + let post_terminated = self.lower_yul_stmts(post)?; + if !post_terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), header)); + } + self.fb.switch_to_block(done); + self.pop_scope(); + Ok(false) + } + YulStmtKind::Switch { + expr, + cases, + default, + } => { + let scrutinee = self.lower_yul_expr(expr)?; + let done = self.fb.append_block(); + for case in cases { + let arm = self.fb.append_block(); + let next = self.fb.append_block(); + let expected = self.lower_yul_lit(&case.lit)?; + let cond = self.fb.insert_inst( + Eq::new(self.module.inst_set(), scrutinee, expected), + Type::I1, + ); + self.fb + .insert_inst_no_result(Br::new(self.module.inst_set(), cond, arm, next)); + self.fb.switch_to_block(arm); + let terminated = self.lower_yul_stmts(&case.body)?; + if !terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + self.fb.switch_to_block(next); + } + if let Some(default) = default { + let terminated = self.lower_yul_stmts(default)?; + if !terminated { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + } else { + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), done)); + } + self.fb.switch_to_block(done); + Ok(false) + } + YulStmtKind::FunctionDef { .. } => Err(TranslationError::new( + "nested inline Yul function definitions are not supported", + )), + YulStmtKind::Leave => { + let ret_ty = self.module.lower_ty(&self.ret)?; + if ret_ty == Type::Unit { + self.fb + .insert_inst_no_result(Return::new_unit(self.module.inst_set())); + } else { + let value = zero_for_type(&mut self.fb, self.module.inst_set(), ret_ty); + self.fb + .insert_inst_no_result(Return::new_single(self.module.inst_set(), value)); + } + Ok(true) + } + YulStmtKind::Break => { + let target = self + .break_targets + .last() + .copied() + .ok_or_else(|| TranslationError::new("inline Yul break outside loop"))?; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), target)); + Ok(true) + } + YulStmtKind::Continue => { + let target = self + .continue_targets + .last() + .copied() + .ok_or_else(|| TranslationError::new("inline Yul continue outside loop"))?; + self.fb + .insert_inst_no_result(Jump::new(self.module.inst_set(), target)); + Ok(true) + } + YulStmtKind::Error => Ok(false), + } + } + + fn lower_yul_expr(&mut self, expr: &YulExpr<'db>) -> Result { + match &expr.kind { + YulExprKind::Lit(lit) => self.lower_yul_lit(lit), + YulExprKind::Ident(name) => { + let binding = self.lookup(&self.yul_name(name))?; + let value = self.fb.use_var(binding.var); + self.coerce(value, Type::I256) + } + YulExprKind::Call { name, args } => { + let name = self.yul_name(name); + if matches!(name.as_str(), "dataoffset" | "datasize") { + let symbol = args + .first() + .and_then(yul_symbol) + .ok_or_else(|| TranslationError::new(format!("{name} expects a symbol")))?; + let sym = if self + .module + .section_objects + .get(&self.scope) + .is_some_and(|current| current == &symbol) + { + SymbolRef::CurrentSection + } else { + SymbolRef::Embed(EmbedSymbol::from(symbol)) + }; + return Ok(if name == "dataoffset" { + self.fb + .insert_inst(SymAddr::new(self.module.inst_set(), sym), Type::I256) + } else { + self.fb + .insert_inst(SymSize::new(self.module.inst_set(), sym), Type::I256) + }); + } + let values = args + .iter() + .map(|arg| self.lower_yul_expr(arg)) + .collect::, _>>()?; + let source_name = name.strip_prefix("usr$").unwrap_or(&name); + if let Some(func) = self + .module + .functions + .get(&key(&self.scope, source_name)) + .copied() + { + let param_tys = self + .fb + .module_builder + .sig(func, |signature| signature.args().to_vec()); + if param_tys.len() != values.len() { + return Err(TranslationError::new(format!( + "inline Yul call to `{source_name}` expects {} arguments, got {}", + param_tys.len(), + values.len() + ))); + } + let values = values + .into_iter() + .zip(param_tys) + .map(|(value, target)| { + if !target.is_integral() { + return Err(TranslationError::new(format!( + "inline Yul cannot pass a word to aggregate parameter `{target:?}` of `{source_name}`" + ))); + } + self.coerce(value, target) + }) + .collect::, _>>()?; + let ret_hull = self + .module + .function_returns + .get(&key(&self.scope, source_name)) + .cloned() + .ok_or_else(|| TranslationError::new("missing function return type"))?; + let ret = self.module.lower_ty(&ret_hull)?; + let call = Call::new(self.module.inst_set(), func, values); + return if ret == Type::Unit { + self.fb.insert_inst_no_result(call); + Ok(self.fb.make_undef_value(Type::Unit)) + } else if ret.is_integral() { + let value = self.fb.insert_inst(call, ret); + self.coerce(value, Type::I256) + } else { + Err(TranslationError::new(format!( + "inline Yul cannot use aggregate return type `{ret:?}` from `{source_name}`" + ))) + }; + } + match self.lower_evm_builtin(&name, &values)? { + BuiltinOutcome::Value(value) => self.coerce(value, Type::I256), + BuiltinOutcome::Unit => Ok(self.fb.make_undef_value(Type::Unit)), + BuiltinOutcome::Terminated => Err(TranslationError::new(format!( + "terminating EVM builtin `{name}` cannot be used as a value" + ))), + } + } + YulExprKind::Error => Ok(self.fb.make_imm_value(I256::zero())), + } + } + + fn lower_yul_lit(&mut self, lit: &YulLitKind) -> Result { + match lit { + YulLitKind::Number(value) | YulLitKind::Hex(value) => self.word_value(value), + YulLitKind::Bool(value) => Ok(self.fb.make_imm_value(I256::from(u8::from(*value)))), + YulLitKind::String(value) => { + let value = value + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .unwrap_or(value); + if value.len() > 32 { + return Err(TranslationError::new( + "inline Yul string literal exceeds one EVM word", + )); + } + let mut bytes = [0u8; 32]; + bytes[..value.len()].copy_from_slice(value.as_bytes()); + Ok(self.fb.make_imm_value(I256::from_be_bytes(&bytes))) + } + YulLitKind::Error => Ok(self.fb.make_imm_value(I256::zero())), + } + } + + fn yul_name(&self, name: &hir::span::SpannedElem<'db, hir::ast::Ident<'db>>) -> String { + (*name.atom()).text(self.module.db).to_owned() + } + + fn lower_evm_builtin( + &mut self, + name: &str, + args: &[ValueId], + ) -> Result { + let arg = |index: usize| { + args.get(index).copied().ok_or_else(|| { + TranslationError::new(format!("EVM builtin `{name}` is missing argument {index}")) + }) + }; + let word = |value| BuiltinOutcome::Value(value); + let outcome = match name { + "memoryguard" => word(arg(0)?), + "add" => word(self.fb.insert_inst( + Add::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "sub" => word(self.fb.insert_inst( + Sub::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "mul" => word(self.fb.insert_inst( + Mul::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "div" => word(self.fb.insert_inst( + EvmUdiv::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "sdiv" => word(self.fb.insert_inst( + EvmSdiv::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "mod" => word(self.fb.insert_inst( + EvmUmod::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "smod" => word(self.fb.insert_inst( + EvmSmod::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "addmod" => word(self.fb.insert_inst( + EvmAddMod::new(self.module.inst_set(), arg(0)?, arg(1)?, arg(2)?), + Type::I256, + )), + "mulmod" => word(self.fb.insert_inst( + EvmMulMod::new(self.module.inst_set(), arg(0)?, arg(1)?, arg(2)?), + Type::I256, + )), + "exp" => word(self.fb.insert_inst( + EvmExp::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "signextend" => word(self.fb.insert_inst( + EvmSignExtend::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "lt" => word( + self.fb + .insert_inst(Lt::new(self.module.inst_set(), arg(0)?, arg(1)?), Type::I1), + ), + "gt" => word( + self.fb + .insert_inst(Gt::new(self.module.inst_set(), arg(0)?, arg(1)?), Type::I1), + ), + "slt" => word( + self.fb + .insert_inst(Slt::new(self.module.inst_set(), arg(0)?, arg(1)?), Type::I1), + ), + "sgt" => word( + self.fb + .insert_inst(Sgt::new(self.module.inst_set(), arg(0)?, arg(1)?), Type::I1), + ), + "eq" => word( + self.fb + .insert_inst(Eq::new(self.module.inst_set(), arg(0)?, arg(1)?), Type::I1), + ), + "iszero" => word( + self.fb + .insert_inst(IsZero::new(self.module.inst_set(), arg(0)?), Type::I1), + ), + "and" => word(self.fb.insert_inst( + And::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "or" => word(self.fb.insert_inst( + Or::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "xor" => word(self.fb.insert_inst( + Xor::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "not" => word( + self.fb + .insert_inst(Not::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "byte" => word(self.fb.insert_inst( + EvmByte::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "shl" => word(self.fb.insert_inst( + Shl::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "shr" => word(self.fb.insert_inst( + Shr::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "sar" => word(self.fb.insert_inst( + Sar::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "clz" => word( + self.fb + .insert_inst(EvmClz::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "keccak256" => word(self.fb.insert_inst( + EvmKeccak256::new(self.module.inst_set(), arg(0)?, arg(1)?), + Type::I256, + )), + "address" => word( + self.fb + .insert_inst(EvmAddress::new(self.module.inst_set()), Type::I256), + ), + "balance" => word( + self.fb + .insert_inst(EvmBalance::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "origin" => word( + self.fb + .insert_inst(EvmOrigin::new(self.module.inst_set()), Type::I256), + ), + "caller" => word( + self.fb + .insert_inst(EvmCaller::new(self.module.inst_set()), Type::I256), + ), + "callvalue" => word( + self.fb + .insert_inst(EvmCallValue::new(self.module.inst_set()), Type::I256), + ), + "calldataload" => word(self.fb.insert_inst( + EvmCalldataLoad::new(self.module.inst_set(), arg(0)?), + Type::I256, + )), + "calldatasize" => word( + self.fb + .insert_inst(EvmCalldataSize::new(self.module.inst_set()), Type::I256), + ), + "calldatacopy" => { + self.fb.insert_inst_no_result(EvmCalldataCopy::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + )); + BuiltinOutcome::Unit + } + "codesize" => word( + self.fb + .insert_inst(EvmCodeSize::new(self.module.inst_set()), Type::I256), + ), + "codecopy" | "datacopy" => { + self.fb.insert_inst_no_result(EvmCodeCopy::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + )); + BuiltinOutcome::Unit + } + "gasprice" => word( + self.fb + .insert_inst(EvmGasPrice::new(self.module.inst_set()), Type::I256), + ), + "extcodesize" => word(self.fb.insert_inst( + EvmExtCodeSize::new(self.module.inst_set(), arg(0)?), + Type::I256, + )), + "extcodecopy" => { + self.fb.insert_inst_no_result(EvmExtCodeCopy::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + )); + BuiltinOutcome::Unit + } + "returndatasize" => word( + self.fb + .insert_inst(EvmReturnDataSize::new(self.module.inst_set()), Type::I256), + ), + "returndatacopy" => { + self.fb.insert_inst_no_result(EvmReturnDataCopy::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + )); + BuiltinOutcome::Unit + } + "extcodehash" => word(self.fb.insert_inst( + EvmExtCodeHash::new(self.module.inst_set(), arg(0)?), + Type::I256, + )), + "blockhash" => word(self.fb.insert_inst( + EvmBlockHash::new(self.module.inst_set(), arg(0)?), + Type::I256, + )), + "coinbase" => word( + self.fb + .insert_inst(EvmCoinBase::new(self.module.inst_set()), Type::I256), + ), + "timestamp" => word( + self.fb + .insert_inst(EvmTimestamp::new(self.module.inst_set()), Type::I256), + ), + "number" => word( + self.fb + .insert_inst(EvmNumber::new(self.module.inst_set()), Type::I256), + ), + "prevrandao" | "difficulty" => word( + self.fb + .insert_inst(EvmPrevRandao::new(self.module.inst_set()), Type::I256), + ), + "gaslimit" => word( + self.fb + .insert_inst(EvmGasLimit::new(self.module.inst_set()), Type::I256), + ), + "chainid" => word( + self.fb + .insert_inst(EvmChainId::new(self.module.inst_set()), Type::I256), + ), + "selfbalance" => word( + self.fb + .insert_inst(EvmSelfBalance::new(self.module.inst_set()), Type::I256), + ), + "basefee" => word( + self.fb + .insert_inst(EvmBaseFee::new(self.module.inst_set()), Type::I256), + ), + "blobhash" => word(self.fb.insert_inst( + EvmBlobHash::new(self.module.inst_set(), arg(0)?), + Type::I256, + )), + "blobbasefee" => word( + self.fb + .insert_inst(EvmBlobBaseFee::new(self.module.inst_set()), Type::I256), + ), + "mload" => word( + self.fb + .insert_inst(EvmMload::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "mstore" => { + self.fb.insert_inst_no_result(EvmMstore::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Unit + } + "mstore8" => { + let value = self.fb.insert_inst( + Trunc::new(self.module.inst_set(), arg(1)?, Type::I8), + Type::I8, + ); + self.fb.insert_inst_no_result(EvmMstore8::new( + self.module.inst_set(), + arg(0)?, + value, + )); + BuiltinOutcome::Unit + } + "sload" => word( + self.fb + .insert_inst(EvmSload::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "sstore" => { + self.fb.insert_inst_no_result(EvmSstore::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Unit + } + "tload" => word( + self.fb + .insert_inst(EvmTload::new(self.module.inst_set(), arg(0)?), Type::I256), + ), + "tstore" => { + self.fb.insert_inst_no_result(EvmTstore::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Unit + } + "msize" => word( + self.fb + .insert_inst(EvmMsize::new(self.module.inst_set()), Type::I256), + ), + "gas" => word( + self.fb + .insert_inst(EvmGas::new(self.module.inst_set()), Type::I256), + ), + "mcopy" => { + self.fb.insert_inst_no_result(EvmMcopy::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + )); + BuiltinOutcome::Unit + } + "log0" => { + self.fb.insert_inst_no_result(EvmLog0::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Unit + } + "log1" => { + self.fb.insert_inst_no_result(EvmLog1::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + )); + BuiltinOutcome::Unit + } + "log2" => { + self.fb.insert_inst_no_result(EvmLog2::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + )); + BuiltinOutcome::Unit + } + "log3" => { + self.fb.insert_inst_no_result(EvmLog3::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + )); + BuiltinOutcome::Unit + } + "log4" => { + self.fb.insert_inst_no_result(EvmLog4::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + arg(5)?, + )); + BuiltinOutcome::Unit + } + "create" => word(self.fb.insert_inst( + EvmCreate::new(self.module.inst_set(), arg(0)?, arg(1)?, arg(2)?), + Type::I256, + )), + "create2" => word(self.fb.insert_inst( + EvmCreate2::new(self.module.inst_set(), arg(0)?, arg(1)?, arg(2)?, arg(3)?), + Type::I256, + )), + "call" => word(self.fb.insert_inst( + EvmCall::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + arg(5)?, + arg(6)?, + ), + Type::I256, + )), + "callcode" => word(self.fb.insert_inst( + EvmCallCode::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + arg(5)?, + arg(6)?, + ), + Type::I256, + )), + "delegatecall" => word(self.fb.insert_inst( + EvmDelegateCall::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + arg(5)?, + ), + Type::I256, + )), + "staticcall" => word(self.fb.insert_inst( + EvmStaticCall::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + arg(2)?, + arg(3)?, + arg(4)?, + arg(5)?, + ), + Type::I256, + )), + "return" => { + self.fb.insert_inst_no_result(EvmReturn::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Terminated + } + "revert" => { + self.fb.insert_inst_no_result(EvmRevert::new( + self.module.inst_set(), + arg(0)?, + arg(1)?, + )); + BuiltinOutcome::Terminated + } + "stop" => { + self.fb + .insert_inst_no_result(EvmStop::new(self.module.inst_set())); + BuiltinOutcome::Terminated + } + "invalid" => { + self.fb + .insert_inst_no_result(EvmInvalid::new(self.module.inst_set())); + BuiltinOutcome::Terminated + } + "selfdestruct" => { + self.fb + .insert_inst_no_result(EvmSelfDestruct::new(self.module.inst_set(), arg(0)?)); + BuiltinOutcome::Terminated + } + "pop" => BuiltinOutcome::Unit, + _ => { + return Err(TranslationError::new(format!( + "unsupported inline Yul/EVM builtin `{name}`" + ))); + } + }; + Ok(outcome) + } + + fn lower_primitive_call( + &mut self, + name: &str, + args: &[ValueId], + result: &HullTy<'db>, + ) -> Result { + let result_ty = self.module.lower_ty(result)?; + let binary = |args: &[ValueId]| -> Result<(ValueId, ValueId), TranslationError> { + let [lhs, rhs] = args else { + return Err(TranslationError::new(format!( + "builtin `{name}` expects two arguments" + ))); + }; + Ok((*lhs, *rhs)) + }; + let value = match name { + "wordFromInteger" | "wordToInteger" => *args.first().ok_or_else(|| { + TranslationError::new(format!("builtin `{name}` expects one argument")) + })?, + "add" | "primAddWord" | "integerAdd" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Add::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "sub" | "subWord" | "integerSub" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Sub::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "mul" | "mulWord" | "integerMul" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Mul::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "div" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(EvmUdiv::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "sdiv" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(EvmSdiv::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "mod" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(EvmUmod::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "smod" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(EvmSmod::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "eq" | "primEqWord" | "integerEq" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Eq::new(self.module.inst_set(), lhs, rhs), Type::I1) + } + "lt" | "integerLt" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Lt::new(self.module.inst_set(), lhs, rhs), Type::I1) + } + "gt" | "gtWord" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Gt::new(self.module.inst_set(), lhs, rhs), Type::I1) + } + "slt" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Slt::new(self.module.inst_set(), lhs, rhs), Type::I1) + } + "and" | "bandWord" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(And::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "or" | "borWord" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Or::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "xor" | "bxorWord" => { + let (lhs, rhs) = binary(args)?; + self.fb + .insert_inst(Xor::new(self.module.inst_set(), lhs, rhs), Type::I256) + } + "not" => { + let value = *args + .first() + .ok_or_else(|| TranslationError::new("not expects one argument"))?; + self.fb + .insert_inst(Not::new(self.module.inst_set(), value), Type::I256) + } + "iszero" => { + let value = *args + .first() + .ok_or_else(|| TranslationError::new("iszero expects one argument"))?; + self.fb + .insert_inst(IsZero::new(self.module.inst_set(), value), Type::I1) + } + "shl" => { + let (bits, value) = binary(args)?; + self.fb + .insert_inst(Shl::new(self.module.inst_set(), bits, value), Type::I256) + } + "shr" => { + let (bits, value) = binary(args)?; + self.fb + .insert_inst(Shr::new(self.module.inst_set(), bits, value), Type::I256) + } + "sar" => { + let (bits, value) = binary(args)?; + self.fb + .insert_inst(Sar::new(self.module.inst_set(), bits, value), Type::I256) + } + "exp" => { + let (base, exponent) = binary(args)?; + self.fb.insert_inst( + EvmExp::new(self.module.inst_set(), base, exponent), + Type::I256, + ) + } + _ => { + return Err(TranslationError::new(format!( + "unsupported Hull/EVM builtin `{name}`" + ))); + } + }; + self.coerce(value, result_ty) + } +} + +fn zero_for_type( + fb: &mut FunctionBuilder, + inst_set: &'static EvmInstSet, + ty: Type, +) -> ValueId { + if ty == Type::Unit { + return fb.make_undef_value(ty); + } + let Type::Compound(compound_ref) = ty else { + return fb.make_imm_value(Immediate::zero(ty)); + }; + let Some(compound) = ty.resolve_compound(fb.ctx()) else { + return fb.make_undef_value(ty); + }; + match compound { + CompoundType::Struct(data) => { + let mut value = fb.make_undef_value(ty); + for (index, field_ty) in data.fields.into_iter().enumerate() { + let field = zero_for_type(fb, inst_set, field_ty); + let index = fb.make_imm_value(I256::from(index)); + value = fb.insert_inst(InsertValue::new(inst_set, value, index, field), ty); + } + value + } + CompoundType::Array { elem, len } => { + let mut value = fb.make_undef_value(ty); + for index in 0..len { + let field = zero_for_type(fb, inst_set, elem); + let index = fb.make_imm_value(I256::from(index)); + value = fb.insert_inst(InsertValue::new(inst_set, value, index, field), ty); + } + value + } + CompoundType::Enum(data) => { + let Some(variant) = data.variants.first() else { + return fb.make_undef_value(ty); + }; + let fields = variant + .fields + .iter() + .copied() + .map(|field_ty| zero_for_type(fb, inst_set, field_ty)) + .collect::>(); + fb.insert_inst( + EnumMake::new(inst_set, ty, EnumVariantRef::new(compound_ref, 0), fields), + ty, + ) + } + CompoundType::Ptr(_) + | CompoundType::ObjRef(_) + | CompoundType::ConstRef(_) + | CompoundType::Func { .. } => fb.make_undef_value(ty), + } +} + +fn is_primitive_name(name: &str) -> bool { + matches!( + name, + "wordFromInteger" + | "wordToInteger" + | "add" + | "primAddWord" + | "integerAdd" + | "sub" + | "subWord" + | "integerSub" + | "mul" + | "mulWord" + | "integerMul" + | "div" + | "sdiv" + | "mod" + | "smod" + | "eq" + | "primEqWord" + | "integerEq" + | "lt" + | "integerLt" + | "gt" + | "gtWord" + | "slt" + | "and" + | "bandWord" + | "or" + | "borWord" + | "xor" + | "bxorWord" + | "not" + | "iszero" + | "shl" + | "shr" + | "sar" + | "exp" + ) +} + +fn is_terminal_evm_builtin(name: &str) -> bool { + matches!( + name, + "return" | "revert" | "stop" | "invalid" | "selfdestruct" + ) +} + +fn yul_symbol(expr: &YulExpr<'_>) -> Option { + match &expr.kind { + YulExprKind::Lit(YulLitKind::String(value)) => Some( + value + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + .unwrap_or(value) + .to_owned(), + ), + _ => None, + } +} + +fn decode_hex_word(hex: &str) -> Result, TranslationError> { + if hex.len() > 64 || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) { + return Err(TranslationError::new(format!( + "invalid 256-bit hexadecimal literal `0x{hex}`" + ))); + } + let padded = if hex.len().is_multiple_of(2) { + hex.to_owned() + } else { + format!("0{hex}") + }; + (0..padded.len()) + .step_by(2) + .map(|index| { + u8::from_str_radix(&padded[index..index + 2], 16) + .map_err(|err| TranslationError::new(err.to_string())) + }) + .collect() +} + +fn assignment_path<'a>(expr: &'a HullExpr<'_>) -> Result<(&'a str, Vec), TranslationError> { + fn go<'a>(expr: &'a HullExpr<'_>, out: &mut Vec) -> Option<&'a str> { + match &expr.kind { + ExprKind::Var(name) => Some(name.as_str()), + ExprKind::Fst(inner) => { + let root = go(inner, out)?; + out.push(0); + Some(root) + } + ExprKind::Snd(inner) => { + let root = go(inner, out)?; + out.push(1); + Some(root) + } + _ => None, + } + } + let mut path = Vec::new(); + let root = go(expr, &mut path) + .ok_or_else(|| TranslationError::new("unsupported Hull assignment target"))?; + Ok((root, path)) +} + +fn product_parts<'a, 'db>( + ty: &'a HullTy<'db>, +) -> Result<(&'a HullTy<'db>, &'a HullTy<'db>), TranslationError> { + match &ty.strip_named().kind { + TyKind::Product(lhs, rhs) => Ok((lhs, rhs)), + _ => Err(TranslationError::new("expected Hull product type")), + } +} + +fn sum_parts<'a, 'db>( + ty: &'a HullTy<'db>, +) -> Result<(&'a HullTy<'db>, &'a HullTy<'db>), TranslationError> { + match &ty.strip_named().kind { + TyKind::Sum(lhs, rhs) => Ok((lhs, rhs)), + _ => Err(TranslationError::new("expected Hull sum type")), + } +} + +fn is_bool_like(ty: &HullTy<'_>) -> bool { + matches!(ty.strip_named().kind, TyKind::Bool) + || matches!( + &ty.strip_named().kind, + TyKind::Sum(lhs, rhs) if is_unit_ty(lhs) && is_unit_ty(rhs) + ) +} + +fn is_unit_ty(ty: &HullTy<'_>) -> bool { + matches!(ty.strip_named().kind, TyKind::Unit) +} + +fn key(scope: &str, name: &str) -> String { + format!("{scope}::{name}") +} + +fn function_symbol(scope: &str, name: &str) -> String { + format!( + "solcore_fn_{}_{}", + encode_symbol_component(scope), + encode_symbol_component(name) + ) +} + +fn entry_symbol(scope: &str) -> String { + format!("solcore_entry_{}", encode_symbol_component(scope)) +} + +fn object_section_name(scope: &str) -> String { + format!("solcore_object_{}", encode_symbol_component(scope)) +} + +fn encode_symbol_component(source: &str) -> String { + use std::fmt::Write as _; + + let mut out = format!("{}_", source.len()); + for byte in source.bytes() { + if byte.is_ascii_alphanumeric() { + out.push(char::from(byte)); + } else { + write!(&mut out, "_{byte:02x}").expect("writing to String cannot fail"); + } + } + out +} + +fn sanitize(source: &str) -> String { + let mut out = String::new(); + for ch in source.chars() { + if ch.is_ascii_alphanumeric() || ch == '_' { + out.push(ch); + } else { + out.push('_'); + } + } + if out.is_empty() { + "anon".to_owned() + } else { + out + } +} diff --git a/crates/sonatina/tests/e2e.rs b/crates/sonatina/tests/e2e.rs new file mode 100644 index 00000000..85745787 --- /dev/null +++ b/crates/sonatina/tests/e2e.rs @@ -0,0 +1,429 @@ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use dir_test::{Fixture, dir_test}; +use hir::ast::item::{ContractItem, FuncKind, FunctionDef, Item, Module}; +use hir_ty::{AbiParam, AbiType}; +use hull::Program; +use nameres::{Db as _, module_id_from_key}; +use parser::parse_file_to_hir; +use solcore_sonatina::translate_hull_program; +use solcore_test_utils::{ + define_frontend_test_db, + e2e::{ + AbiShape, E2eFailure, FailureKind, ResolvedE2eCall, e2e_enabled, e2e_pipeline_only, + e2e_required, encode_hex, parse_e2e_directive, resolve_e2e_comments, + with_shared_evm_harness, + }, + load_fixture_case_with_file_urls, load_reachable_modules_with_file_urls, + repo_root_from_manifest, +}; +use sonatina_codegen::{EvmCompile, OptLevel}; +use specialize::{ + MonoEntry, MonoItem, MonoModule, MonoRuntimeMainOrigin, SpecializeOptions, specialize_module, +}; + +define_frontend_test_db!(TestDb, hir_ty); + +type CompiledFixture = (Vec<(OptLevel, Vec)>, Vec); + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/../../tests/e2e", + glob: "**/main.solc" +)] +fn sonatina_evm_e2e(fixture: Fixture<&str>) { + if !e2e_enabled() { + assert!( + !e2e_required(), + "E2E_REQUIRED=1 requires E2E=1; refusing to skip Sonatina E2E" + ); + return; + } + + let path = PathBuf::from(fixture.path()); + let result = lower_and_compile(&path).and_then(|(creations, calls)| { + if e2e_pipeline_only() { + return Ok(()); + } + with_shared_evm_harness(|harness| { + let Some(harness) = harness else { + return Ok(()); + }; + for (opt_level, creation) in creations { + harness + .execute_deployed_calls(&encode_hex(&creation), &calls) + .map_err(|failure| { + E2eFailure::new( + failure.kind, + format!("{opt_level:?} execution failed: {}", failure.message), + ) + })?; + } + Ok(()) + }) + }); + + result.unwrap_or_else(|failure| { + panic!( + "Sonatina E2E fixture `{}` failed: {failure}", + path.display() + ) + }); +} + +fn lower_and_compile(path: &Path) -> Result { + let lowered = lower_fixture(path)?; + let creations = [OptLevel::O0, OptLevel::O2] + .into_iter() + .map(|opt_level| { + compile_creation(lowered.db, &lowered.program, opt_level) + .map(|creation| (opt_level, creation)) + }) + .collect::, _>>()?; + Ok((creations, lowered.calls)) +} + +fn compile_creation( + db: &'static TestDb, + program: &Program<'static>, + opt_level: OptLevel, +) -> Result, E2eFailure> { + let module = translate_hull_program(db, program).map_err(|error| { + pipeline_error(format!( + "Hull-to-Sonatina translation failed: {}", + error.message() + )) + })?; + let mut artifacts = EvmCompile::new(module) + .with_opt_level(opt_level) + .compile() + .map_err(|errors| { + pipeline_error(format!("Sonatina {opt_level:?} codegen failed: {errors:?}")) + })?; + if artifacts.len() != 1 { + return Err(pipeline_error(format!( + "expected one Sonatina object artifact, got {} ({})", + artifacts.len(), + artifacts + .iter() + .map(|artifact| artifact.object.0.as_str()) + .collect::>() + .join(", ") + ))); + } + let artifact = artifacts.pop().expect("artifact count checked"); + artifact + .sections + .into_iter() + .find_map(|(name, section)| (name.0.as_str() == "init").then_some(section.bytes)) + .ok_or_else(|| { + pipeline_error(format!( + "Sonatina object `{}` has no init section", + artifact.object.0 + )) + }) +} + +struct LoweredFixture { + db: &'static TestDb, + program: Program<'static>, + calls: Vec, +} + +fn lower_fixture(path: &Path) -> Result { + let db = Box::leak(Box::new(TestDb::default())); + let repo = repo_root(); + let case_dir = path + .parent() + .ok_or_else(|| pipeline_error(format!("fixture {} has no parent", path.display())))?; + let entry = load_fixture_case_with_file_urls(db, case_dir, &repo, BTreeMap::new()); + load_reachable_modules_with_file_urls(db, entry.clone()); + let db: &'static TestDb = &*db; + let entry_id = module_id_from_key(db, &entry); + let file = db + .module_file(entry_id) + .ok_or_else(|| pipeline_error("entry source file is missing"))?; + let hir = parse_file_to_hir(db, file).module(db); + let specialized = specialize_module(db, hir, SpecializeOptions::default()); + finish_lowering(db, specialized) +} + +fn finish_lowering( + db: &'static TestDb, + specialized: specialize::SpecializeOutput<'static>, +) -> Result { + if !specialized.diagnostics.is_empty() { + return Err(pipeline_error(format!( + "specialization diagnostics: {:?}", + specialized.diagnostics + ))); + } + let source = parse_file_to_hir(db, specialized.module.module.file(db)).module(db); + let directives = resolve_fixture_directives(db, source, &specialized.module)?; + let emitted = hull::emit_module(db, &specialized.module, hull::EmitOptions::default()); + if !emitted.diagnostics.is_empty() { + return Err(pipeline_error(format!( + "Hull emission diagnostics: {:?}", + emitted.diagnostics + ))); + } + let diagnostics = hull::check_program_with_db(db, &emitted.program); + if !diagnostics.is_empty() { + return Err(pipeline_error(format!( + "Hull check diagnostics: {diagnostics:?}" + ))); + } + + let program = contract_program(&emitted.program, &directives.contract)?; + Ok(LoweredFixture { + db, + program, + calls: directives.calls, + }) +} + +struct ResolvedFixtureDirectives { + contract: String, + calls: Vec, +} + +fn resolve_fixture_directives( + db: &'static TestDb, + source: Module<'static>, + specialized: &MonoModule<'static>, +) -> Result { + let mut selected_contract = None::; + let mut calls = Vec::new(); + + for item in source.items(db) { + match item { + Item::FunctionDef(function) => { + reject_non_dispatch_directives(db, *function, "top-level function")?; + } + Item::InstanceDef(instance) => { + for function in instance.methods(db) { + reject_non_dispatch_directives(db, *function, "impl method")?; + } + } + Item::ContractDef(contract) => { + let contract_name = contract.name_elem(db).atom().text(db).to_owned(); + let contract_def = contract.def_id_value(db); + let surface = hir_ty::contract_dispatch_surface_for_module(db, source, *contract); + let mono_contract = specialized + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) if contract.def == contract_def => { + Some(contract) + } + _ => None, + }) + .ok_or_else(|| { + directive_error(format!( + "{contract_name}: specialized contract metadata is missing" + )) + })?; + let has_dispatch_runtime = mono_contract.entries.iter().any(|entry| { + matches!( + entry, + MonoEntry::RuntimeMain { + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } + ) + }); + + for item in contract.items(db) { + let ContractItem::FunctionDef(function) = item else { + continue; + }; + let comments = comment_texts(db, *function); + let function_name = function_name(db, *function); + let context = format!("{contract_name}::{function_name}"); + if !contains_directive(&comments, &context)? { + continue; + } + if function.kind(db) != FuncKind::Function { + return Err(directive_error(format!( + "{context}: directives may only target ordinary public functions" + ))); + } + if !function.sig(db).is_abi_visible() { + return Err(directive_error(format!( + "{context}: directive target is private and has no external selector" + ))); + } + if function_name == "main" { + return Err(directive_error(format!( + "{context}: contract runtime `main` is not a selector-dispatched method" + ))); + } + if contract.has_runtime_main(db) { + return Err(directive_error(format!( + "{context}: contract `{contract_name}` defines a runtime `main`, so selector dispatch is disabled" + ))); + } + if !has_dispatch_runtime { + return Err(directive_error(format!( + "{context}: specialization did not emit the generated selector dispatcher" + ))); + } + + let def = function.def_id_value(db); + let mut methods = surface.methods.iter().filter(|method| method.def == def); + let method = match (methods.next(), methods.next()) { + (Some(method), None) => method, + (None, _) => { + return Err(directive_error(format!( + "{context}: typed dispatch metadata has no matching source DefId" + ))); + } + (Some(_), Some(_)) => { + return Err(directive_error(format!( + "{context}: typed dispatch metadata is ambiguous for its source DefId" + ))); + } + }; + + match &selected_contract { + None => selected_contract = Some(contract_name.clone()), + Some(selected) if selected == &contract_name => {} + Some(selected) => { + return Err(directive_error(format!( + "one fixture may target only one deployed contract; found `{selected}` and `{contract_name}`" + ))); + } + } + let inputs = abi_shapes(&method.inputs); + let outputs = abi_shapes(&method.outputs); + calls.extend(resolve_e2e_comments( + method.signature.clone(), + method.selector.0, + &inputs, + &outputs, + comments.iter().copied(), + )?); + } + } + _ => {} + } + } + + let contract = selected_contract.ok_or_else(|| { + directive_error("fixture contains no E2E directives on selector-dispatched methods") + })?; + if calls.is_empty() { + return Err(directive_error( + "fixture contains no executable E2E directives", + )); + } + Ok(ResolvedFixtureDirectives { contract, calls }) +} + +fn reject_non_dispatch_directives( + db: &'static TestDb, + function: FunctionDef<'static>, + kind: &str, +) -> Result<(), E2eFailure> { + let name = function_name(db, function); + let context = format!("{kind} `{name}`"); + let comments = comment_texts(db, function); + if contains_directive(&comments, &context)? { + return Err(directive_error(format!( + "{context}: directives require a public contract selector method" + ))); + } + Ok(()) +} + +fn comment_texts(db: &'static TestDb, function: FunctionDef<'static>) -> Vec<&'static str> { + function + .leading_comments(db) + .iter() + .map(|comment| comment.text.as_str()) + .collect() +} + +fn contains_directive(comments: &[&str], context: &str) -> Result { + let mut found = false; + for comment in comments { + match parse_e2e_directive(comment) { + Ok(Some(_)) => found = true, + Ok(None) => {} + Err(error) => { + return Err(directive_error(format!("{context}: {error}"))); + } + } + } + Ok(found) +} + +fn abi_shapes(params: &[AbiParam]) -> Vec { + params.iter().map(abi_shape).collect() +} + +fn abi_shape(param: &AbiParam) -> AbiShape { + match ¶m.ty { + AbiType::Uint256 => AbiShape::Word, + AbiType::Bool => AbiShape::Bool, + AbiType::Unit => AbiShape::Unit, + AbiType::Tuple => AbiShape::Tuple(abi_shapes(¶m.components)), + AbiType::Named(name) => match name.as_str() { + "uint" | "uint256" | "word" => AbiShape::Word, + "bool" => AbiShape::Bool, + "address" => AbiShape::Address, + "bytes32" => AbiShape::Bytes32, + _ => AbiShape::Unsupported(name.clone()), + }, + AbiType::String => AbiShape::Unsupported("string".to_owned()), + AbiType::Unsupported => AbiShape::Unsupported(param.ty.to_string()), + } +} + +fn contract_program( + program: &Program<'static>, + contract: &str, +) -> Result, E2eFailure> { + let deployer = format!("{contract}Deploy"); + let mut objects = program + .objects + .iter() + .filter(|object| object.name.as_str() == deployer); + let object = match (objects.next(), objects.next()) { + (Some(object), None) => object.clone(), + (None, _) => { + return Err(pipeline_error(format!( + "Hull deploy object `{deployer}` was not emitted" + ))); + } + (Some(_), Some(_)) => { + return Err(pipeline_error(format!( + "Hull deploy object `{deployer}` is ambiguous" + ))); + } + }; + Ok(Program { + span: program.span, + entry_points: Vec::new(), + functions: Vec::new(), + objects: vec![object], + }) +} + +fn function_name(db: &'static TestDb, function: FunctionDef<'static>) -> String { + function.sig(db).name.atom().text(db).to_owned() +} + +fn directive_error(message: impl Into) -> E2eFailure { + E2eFailure::new(FailureKind::Directive, message) +} + +fn pipeline_error(message: impl Into) -> E2eFailure { + E2eFailure::new(FailureKind::Pipeline, message) +} + +fn repo_root() -> PathBuf { + repo_root_from_manifest(env!("CARGO_MANIFEST_DIR")) +} diff --git a/crates/sonatina/tests/lowering.rs b/crates/sonatina/tests/lowering.rs new file mode 100644 index 00000000..b1a52485 --- /dev/null +++ b/crates/sonatina/tests/lowering.rs @@ -0,0 +1,564 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use hir::{ + anchor::DefLocationTable, + diag::Offset, + input::SourceFile, + span::{AnchorId, Span}, +}; +use hull::{ + Alt, Arg, CodeBlock, Con, Expr, ExprKind, Function, Object, Pat, PatKind, Program, Stmt, + StmtKind, Ty, +}; +use nameres::{Db as _, ModuleTree, module_id_from_key}; +use parser::parse_file_to_hir; +use solcore_sonatina::{render_hull_program, translate_hull_program}; +use solcore_test_utils::{ + FrontendTestDb, define_frontend_test_db, load_main_source, load_reachable_modules, + module_fs_snapshot_for_roots, repo_root_from_manifest, +}; +use sonatina_ir::{Module, ir_writer::ModuleWriter}; +use sonatina_verifier::{VerificationLevel, VerifierConfig, verify_module}; +use specialize::{SpecializeOptions, specialize_module}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +define_frontend_test_db!(SourceTestDb, hir_ty); + +fn test_span<'db>(db: &'db TestDb) -> Span<'db> { + let file = SourceFile::new( + db, + "memory:///sonatina_lowering.solc" + .parse() + .expect("valid URL"), + Some(String::new()), + ); + Span::new(AnchorId::root(db, file), Offset::new(0), Offset::new(0)) +} + +#[test] +fn lowers_word_bool_and_structural_aggregates_to_verified_ir() { + let db = TestDb::default(); + let span = test_span(&db); + let word = Ty::word(span); + let bool_ty = Ty::bool(span); + let pair = Ty::product(span, word.clone(), bool_ty.clone()); + let sum = Ty::sum(span, Ty::unit(span), pair.clone()); + let program = Program { + span, + entry_points: Vec::new(), + functions: vec![ + Function { + span, + name: "id".into(), + args: vec![Arg { + span, + name: "x".into(), + ty: word.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr::var(span, "x", word.clone())), + }], + }, + Function { + span, + name: "choose".into(), + args: vec![Arg { + span, + name: "flag".into(), + ty: bool_ty.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr { + span, + ty: word.clone(), + kind: ExprKind::If { + target: word.clone(), + cond: Box::new(Expr::var(span, "flag", bool_ty)), + then_expr: Box::new(Expr::word(span, "1")), + else_expr: Box::new(Expr::word(span, "0")), + }, + }), + }], + }, + Function { + span, + name: "aggregate_id".into(), + args: vec![Arg { + span, + name: "x".into(), + ty: sum.clone(), + }], + ret: sum.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr::var(span, "x", sum)), + }], + }, + ], + objects: Vec::new(), + }; + + let ir = render_hull_program(&db, &program).expect("verified Sonatina lowering"); + assert!(ir.contains("target = \"evm-ethereum-osaka\""), "{ir}"); + assert!(ir.contains("i256"), "{ir}"); + assert!(ir.contains("i1"), "{ir}"); + assert!(ir.contains("type @solcore_product"), "{ir}"); + assert!(ir.contains("enum"), "{ir}"); + assert!(ir.contains(" br ") || ir.contains("\n br "), "{ir}"); +} + +#[test] +fn hull_function_symbols_are_injective_and_separate_from_section_entries() { + let db = TestDb::default(); + let span = test_span(&db); + let unit = Ty::unit(span); + let function = |name: &'static str| Function { + span, + name: name.into(), + args: Vec::new(), + ret: unit.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr::unit(span)), + }], + }; + let program = Program { + span, + entry_points: Vec::new(), + functions: vec![function("foo$bar"), function("foo_bar"), function("entry")], + objects: Vec::new(), + }; + + let ir = render_hull_program(&db, &program).expect("collision-free Sonatina lowering"); + assert!( + ir.contains("solcore_fn_12_root_2eruntime_7_foo_24bar"), + "{ir}" + ); + assert!( + ir.contains("solcore_fn_12_root_2eruntime_7_foo_5fbar"), + "{ir}" + ); + assert!(ir.contains("solcore_fn_12_root_2eruntime_5_entry"), "{ir}"); + assert!(ir.contains("solcore_entry_12_root_2eruntime"), "{ir}"); +} + +#[test] +fn aggregate_locals_are_zero_initialized_recursively() { + let db = TestDb::default(); + let span = test_span(&db); + let word = Ty::word(span); + let pair = Ty::product(span, word.clone(), word.clone()); + let sum = Ty::sum(span, word.clone(), word.clone()); + let pair_var = || Expr::var(span, "pair", pair.clone()); + let program = Program { + span, + entry_points: Vec::new(), + functions: vec![ + Function { + span, + name: "main".into(), + args: Vec::new(), + ret: word.clone(), + body: vec![ + Stmt { + span, + kind: StmtKind::Let { + name: "pair".into(), + ty: pair.clone(), + }, + }, + Stmt { + span, + kind: StmtKind::Assign { + lhs: Expr { + span, + ty: word.clone(), + kind: ExprKind::Fst(Box::new(pair_var())), + }, + rhs: Expr::word(span, "7"), + }, + }, + Stmt { + span, + kind: StmtKind::Return(Expr { + span, + ty: word.clone(), + kind: ExprKind::Snd(Box::new(pair_var())), + }), + }, + ], + }, + Function { + span, + name: "zero_sum".into(), + args: Vec::new(), + ret: sum.clone(), + body: vec![ + Stmt { + span, + kind: StmtKind::Let { + name: "sum".into(), + ty: sum.clone(), + }, + }, + Stmt { + span, + kind: StmtKind::Return(Expr::var(span, "sum", sum)), + }, + ], + }, + ], + objects: Vec::new(), + }; + + let ir = render_hull_program(&db, &program).expect("verified aggregate zero lowering"); + let zero_field_inserts = ir + .lines() + .filter(|line| line.contains("insert_value") && line.trim_end().ends_with("0.i256;")) + .count(); + assert!(zero_field_inserts >= 2, "{ir}"); + assert!(ir.contains("enum.make") && ir.contains("0.i256"), "{ir}"); +} + +#[test] +fn all_sibling_and_nested_hull_objects_become_embedded_sections() { + let db = TestDb::default(); + let span = test_span(&db); + let object = |name: &'static str, inners| Object { + span, + name: name.into(), + code: CodeBlock { + span, + stmts: Vec::new(), + functions: Vec::new(), + }, + inners, + }; + let grandchild = object("Grandchild", Vec::new()); + let sibling = object("Sibling", vec![grandchild]); + let runtime = object("Runtime", Vec::new()); + let program = Program { + span, + entry_points: Vec::new(), + functions: Vec::new(), + objects: vec![object("Root", vec![runtime, sibling])], + }; + + let ir = render_hull_program(&db, &program).expect("complete nested object lowering"); + assert!(ir.contains("embed .runtime as &Runtime"), "{ir}"); + assert!(ir.contains("as &Sibling"), "{ir}"); + assert!(ir.contains("as &Grandchild"), "{ir}"); + assert!(ir.matches("section ").count() >= 4, "{ir}"); +} + +#[test] +fn lowers_direct_nary_injections_matches_and_terminal_builtins() { + let db = TestDb::default(); + let span = test_span(&db); + let word = Ty::word(span); + let three_way = Ty::sum( + span, + word.clone(), + Ty::sum(span, word.clone(), word.clone()), + ); + let in_k = |index, value| Expr { + span, + ty: three_way.clone(), + kind: ExprKind::InK { + index, + target: three_way.clone(), + value: Box::new(Expr::word(span, value)), + }, + }; + let alt = |index, result| Alt { + span, + pat: Pat { + span, + kind: PatKind::Con(Con::InK(index)), + }, + binder: format!("value{index}").into(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr::word(span, result)), + }], + }; + let program = Program { + span, + entry_points: Vec::new(), + functions: vec![ + Function { + span, + name: "pick".into(), + args: vec![Arg { + span, + name: "choice".into(), + ty: three_way.clone(), + }], + ret: word.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Match { + target: three_way.clone(), + scrutinee: Expr::var(span, "choice", three_way.clone()), + alts: vec![alt(0, "10"), alt(1, "20"), alt(2, "30")], + }, + }], + }, + Function { + span, + name: "halt".into(), + args: Vec::new(), + ret: Ty::unit(span), + body: vec![ + Stmt { + span, + kind: StmtKind::Expr(Expr { + span, + ty: Ty::unit(span), + kind: ExprKind::Call { + callee: "stop".into(), + args: Vec::new(), + }, + }), + }, + Stmt { + span, + kind: StmtKind::Return(Expr::unit(span)), + }, + ], + }, + Function { + span, + name: "main".into(), + args: Vec::new(), + ret: word.clone(), + body: vec![Stmt { + span, + kind: StmtKind::Return(Expr { + span, + ty: word, + kind: ExprKind::Call { + callee: "pick".into(), + args: vec![in_k(2, "42")], + }, + }), + }], + }, + ], + objects: Vec::new(), + }; + + let ir = render_hull_program(&db, &program).expect("verified n-ary Sonatina lowering"); + assert!(ir.matches("enum.make").count() >= 2, "{ir}"); + assert!(ir.contains("enum.is_variant"), "{ir}"); + assert!(ir.contains("evm_stop;"), "{ir}"); +} + +#[test] +fn source_main_lowers_through_hull_to_verified_ir() { + let (_, ir) = lower_source( + r#" +contract SimpleMain { + function main() returns (word) { + return 42; + } +} +"#, + ); + + assert!(ir.contains("target = \"evm-ethereum-osaka\""), "{ir}"); + assert!(ir.contains("object @SimpleMainDeploy"), "{ir}"); + assert!(ir.contains("42.i256"), "{ir}"); + insta::assert_snapshot!("source_main_ir", ir); +} + +#[test] +fn source_bool_product_sum_and_branches_lower_to_verified_ir() { + let (_, ir) = lower_source( + r#" +contract AggregateContract { + enum Choice { Left(word, word), Right(word) } + + function runtime_flag() returns (bool) { + let raw: word; + assembly { raw := callvalue() } + match (raw) { case 0 { return false; } default { return true; } } + } + + function choose(flag: bool, x: word, y: word) returns (Choice) { + if (flag) { + return Choice.Left(x, y); + } else { + return Choice.Right(y); + } + } + + function unwrap(value: Choice) returns (word) { + match (value) { case Choice.Left(x, y) { return x; } case Choice.Right(x) { return x; } } + } + + function main() returns (word) { + return unwrap(choose(runtime_flag(), 1, 42)); + } +} +"#, + ); + + assert!(ir.contains("i1"), "{ir}"); + assert!(ir.contains("type @solcore_product"), "{ir}"); + assert!(ir.contains("enum"), "{ir}"); + assert!(ir.contains("enum.make"), "{ir}"); + assert!(ir.contains("enum.extract"), "{ir}"); + assert!(ir.contains(" br ") || ir.contains("\n br "), "{ir}"); + insta::assert_snapshot!("source_aggregate_ir", ir); +} + +#[test] +fn contract_object_data_symbols_and_inline_evm_lower_to_verified_ir() { + let (_, ir) = lower_source( + r#" +contract MemoryContract { + function main() returns (word) { + let result: word; + assembly { + mstore(0, 42) + result := mload(0) + } + return result; + } +} +"#, + ); + + assert!(ir.contains("object @MemoryContractDeploy"), "{ir}"); + assert!(ir.contains("embed .runtime as &MemoryContract"), "{ir}"); + // Hull deployment's dataoffset/datasize become Sonatina embed-symbol ops. + assert!(ir.contains("sym_addr &MemoryContract"), "{ir}"); + assert!(ir.contains("sym_size &MemoryContract"), "{ir}"); + assert!(ir.contains("evm_mstore "), "{ir}"); + assert!(ir.contains("evm_mload "), "{ir}"); +} + +#[test] +fn contract_storage_load_and_store_lower_to_snapshotted_verified_ir() { + let (_, ir) = lower_source( + r#" +contract StorageContract { + value: word; + + function update(next: word) returns (word) { + value = next; + return value; + } + + function main() returns (word) { + return update(42); + } +} +"#, + ); + + assert!(ir.contains("evm_sstore "), "{ir}"); + assert!(ir.contains("evm_sload "), "{ir}"); + insta::assert_snapshot!("source_storage_ir", ir); +} + +#[test] +fn inline_yul_for_init_binding_remains_in_loop_scope() { + let (_, ir) = lower_source( + r#" +contract LoopContract { + function main() returns (word) { + let result: word; + assembly { + result := 0 + for { let i := 0 } lt(i, 3) { i := add(i, 1) } { + result := add(result, i) + } + } + return result; + } +} +"#, + ); + + assert!(ir.contains("phi"), "{ir}"); + assert!(ir.contains("jump"), "{ir}"); +} + +fn lower_source(source: &str) -> (Module, String) { + let db = Box::leak(Box::new(SourceTestDb::default())); + let entry = load_main_source(db, source); + let repo = repo_root_from_manifest(env!("CARGO_MANIFEST_DIR")); + let main_root = PathBuf::from("/main"); + let std_root = repo.join("std"); + let tree = ModuleTree::new(&*db, main_root, std_root.clone(), BTreeMap::new()); + db.set_module_tree(tree); + let snapshot = module_fs_snapshot_for_roots(&*db, [std_root.as_path()]); + db.set_module_fs_snapshot(snapshot); + load_reachable_modules(db, entry.clone()); + + let entry_id = module_id_from_key(&*db, &entry); + let _ = nameres::resolve_reachable_full(&*db, entry_id); + assert_eq!( + nameres::reachable_diagnostics(&*db, entry_id), + &[], + "name-resolution diagnostics" + ); + assert_eq!( + hir_ty::infer::reachable_typeck_diagnostics(&*db, entry_id), + &[], + "type-checking diagnostics" + ); + + let file = db.module_file(entry_id).expect("entry source file"); + let hir = parse_file_to_hir(&*db, file).module(&*db); + let specialized = specialize_module(&*db, hir, SpecializeOptions::default()); + assert_eq!( + specialized.diagnostics, + Vec::new(), + "specialization diagnostics" + ); + let emitted = hull::emit_module(&*db, &specialized.module, hull::EmitOptions::default()); + assert_eq!(emitted.diagnostics, Vec::new(), "Hull emission diagnostics"); + assert_eq!( + hull::check_program_with_db(&*db, &emitted.program), + Vec::new(), + "Hull check diagnostics" + ); + + let module = translate_hull_program(&*db, &emitted.program).expect("Sonatina lowering"); + assert_verified(&module); + let ir = ModuleWriter::new(&module).dump_string(); + (module, ir) +} + +fn assert_verified(module: &Module) { + let report = verify_module(module, &VerifierConfig::for_level(VerificationLevel::Full)); + assert!( + !report.has_errors(), + "Sonatina verification failed:\n{report}" + ); +} diff --git a/crates/sonatina/tests/snapshots/lowering__source_aggregate_ir.snap b/crates/sonatina/tests/snapshots/lowering__source_aggregate_ir.snap new file mode 100644 index 00000000..9895aaf9 --- /dev/null +++ b/crates/sonatina/tests/snapshots/lowering__source_aggregate_ir.snap @@ -0,0 +1,152 @@ +--- +source: crates/sonatina/tests/lowering.rs +expression: ir +--- +target = "evm-ethereum-osaka" + +type @solcore_product_0 = {i256, i256}; +type @solcore_sum_1 = enum { + #inl(@solcore_product_0), + #inr(i256), +}; + +func private %solcore_fn_12_object0_2einit_39_main_5fAggregateContract_5f_5fstart_5fdd9a737e0() { + block0: + evm_mstore 64.i256 128.i256; + v2.i256 = evm_code_size; + v3.i256 = sym_size .; + v4.i1 = lt v2 v3; + v5.i256 = zext v4 i256; + v7.i1 = eq v5 0.i256; + v8.i1 = is_zero v7; + br v8 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + v9.i256 = evm_call_value; + v10.i1 = eq v9 0.i256; + v11.i1 = is_zero v10; + br v11 block3 block4; + + block3: + evm_mstore 0.i256 3046674083.i256; + evm_revert 28.i256 4.i256; + + block4: + v17.i256 = sym_size &AggregateContract; + v18.i256 = sym_addr &AggregateContract; + v19.i256 = sym_size &AggregateContract; + evm_code_copy 0.i256 v18 v19; + evm_return 0.i256 v17; +} + +func public %solcore_entry_12_object0_2einit() { + block0: + call %solcore_fn_12_object0_2einit_39_main_5fAggregateContract_5f_5fstart_5fdd9a737e0; + return; +} + +func private %solcore_fn_19_object0_2einner0_2einit_39_main_5fAggregateContract_5fchoose_5fd2b849423(v0.i1, v1.i256, v2.i256) -> @solcore_sum_1 { + block0: + v4.i1 = eq v0 1.i1; + br v4 block2 block3; + + block1: + v22.@solcore_product_0 = insert_value undef.@solcore_product_0 0.i256 0.i256; + v23.@solcore_product_0 = insert_value v22 1.i256 0.i256; + v24.@solcore_sum_1 = enum.make @solcore_sum_1 #inl v23; + return v24; + + block2: + v11.@solcore_product_0 = insert_value undef.@solcore_product_0 0.i256 v1; + v13.@solcore_product_0 = insert_value v11 1.i256 v2; + v14.@solcore_sum_1 = enum.make @solcore_sum_1 #inl v13; + return v14; + + block3: + v16.i1 = eq v0 0.i1; + br v16 block4 block5; + + block4: + v20.@solcore_sum_1 = enum.make @solcore_sum_1 #inr v2; + return v20; + + block5: + jump block1; +} + +func private %solcore_fn_19_object0_2einner0_2einit_37_main_5fAggregateContract_5fmain_5fd0c3f2f7f() -> i256 { + block0: + v0.i1 = call %solcore_fn_19_object0_2einner0_2einit_45_main_5fAggregateContract_5fruntime_5fflag_5fd45392039; + v3.@solcore_sum_1 = call %solcore_fn_19_object0_2einner0_2einit_39_main_5fAggregateContract_5fchoose_5fd2b849423 v0 1.i256 42.i256; + v4.i256 = call %solcore_fn_19_object0_2einner0_2einit_39_main_5fAggregateContract_5funwrap_5fdf2d4e987 v3; + return v4; +} + +func private %solcore_fn_19_object0_2einner0_2einit_45_main_5fAggregateContract_5fruntime_5fflag_5fd45392039() -> i1 { + block0: + v1.i256 = evm_call_value; + v2.i1 = eq v1 0.i256; + br v2 block2 block3; + + block1: + unreachable; + + block2: + return 0.i1; + + block3: + jump block4; + + block4: + return 1.i1; +} + +func private %solcore_fn_19_object0_2einner0_2einit_39_main_5fAggregateContract_5funwrap_5fdf2d4e987(v0.@solcore_sum_1) -> i256 { + block0: + v1.i1 = enum.is_variant v0 #inl; + br v1 block2 block3; + + block1: + return 0.i256; + + block2: + enum.assert_variant v0 #inl; + v3.@solcore_product_0 = enum.extract v0 #inl 0.i256; + v5.@solcore_product_0 = insert_value undef.@solcore_product_0 0.i256 0.i256; + v7.@solcore_product_0 = insert_value v5 1.i256 0.i256; + v8.i256 = extract_value v3 0.i256; + v9.i256 = extract_value v3 1.i256; + return v8; + + block3: + v10.i1 = enum.is_variant v0 #inr; + br v10 block4 block5; + + block4: + enum.assert_variant v0 #inr; + v11.i256 = enum.extract v0 #inr 0.i256; + return v11; + + block5: + jump block1; +} + +func public %solcore_entry_19_object0_2einner0_2einit() { + block0: + v0.i256 = call %solcore_fn_19_object0_2einner0_2einit_37_main_5fAggregateContract_5fmain_5fd0c3f2f7f; + return; +} + + +object @AggregateContractDeploy { + section init { + entry %solcore_entry_12_object0_2einit; + embed .runtime as &AggregateContract; + } + section runtime { + entry %solcore_entry_19_object0_2einner0_2einit; + } +} diff --git a/crates/sonatina/tests/snapshots/lowering__source_main_ir.snap b/crates/sonatina/tests/snapshots/lowering__source_main_ir.snap new file mode 100644 index 00000000..a728a7e1 --- /dev/null +++ b/crates/sonatina/tests/snapshots/lowering__source_main_ir.snap @@ -0,0 +1,65 @@ +--- +source: crates/sonatina/tests/lowering.rs +expression: ir +--- +target = "evm-ethereum-osaka" + +func private %solcore_fn_12_object0_2einit_32_main_5fSimpleMain_5f_5fstart_5fd6da5cf14() { + block0: + evm_mstore 64.i256 128.i256; + v2.i256 = evm_code_size; + v3.i256 = sym_size .; + v4.i1 = lt v2 v3; + v5.i256 = zext v4 i256; + v7.i1 = eq v5 0.i256; + v8.i1 = is_zero v7; + br v8 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + v9.i256 = evm_call_value; + v10.i1 = eq v9 0.i256; + v11.i1 = is_zero v10; + br v11 block3 block4; + + block3: + evm_mstore 0.i256 3046674083.i256; + evm_revert 28.i256 4.i256; + + block4: + v17.i256 = sym_size &SimpleMain; + v18.i256 = sym_addr &SimpleMain; + v19.i256 = sym_size &SimpleMain; + evm_code_copy 0.i256 v18 v19; + evm_return 0.i256 v17; +} + +func public %solcore_entry_12_object0_2einit() { + block0: + call %solcore_fn_12_object0_2einit_32_main_5fSimpleMain_5f_5fstart_5fd6da5cf14; + return; +} + +func private %solcore_fn_19_object0_2einner0_2einit_30_main_5fSimpleMain_5fmain_5fda559e35b() -> i256 { + block0: + return 42.i256; +} + +func public %solcore_entry_19_object0_2einner0_2einit() { + block0: + v0.i256 = call %solcore_fn_19_object0_2einner0_2einit_30_main_5fSimpleMain_5fmain_5fda559e35b; + return; +} + + +object @SimpleMainDeploy { + section init { + entry %solcore_entry_12_object0_2einit; + embed .runtime as &SimpleMain; + } + section runtime { + entry %solcore_entry_19_object0_2einner0_2einit; + } +} diff --git a/crates/sonatina/tests/snapshots/lowering__source_storage_ir.snap b/crates/sonatina/tests/snapshots/lowering__source_storage_ir.snap new file mode 100644 index 00000000..e5eab913 --- /dev/null +++ b/crates/sonatina/tests/snapshots/lowering__source_storage_ir.snap @@ -0,0 +1,73 @@ +--- +source: crates/sonatina/tests/lowering.rs +expression: ir +--- +target = "evm-ethereum-osaka" + +func private %solcore_fn_12_object0_2einit_37_main_5fStorageContract_5f_5fstart_5fd31013ee5() { + block0: + evm_mstore 64.i256 128.i256; + v2.i256 = evm_code_size; + v3.i256 = sym_size .; + v4.i1 = lt v2 v3; + v5.i256 = zext v4 i256; + v7.i1 = eq v5 0.i256; + v8.i1 = is_zero v7; + br v8 block1 block2; + + block1: + evm_revert 0.i256 0.i256; + + block2: + v9.i256 = evm_call_value; + v10.i1 = eq v9 0.i256; + v11.i1 = is_zero v10; + br v11 block3 block4; + + block3: + evm_mstore 0.i256 3046674083.i256; + evm_revert 28.i256 4.i256; + + block4: + v17.i256 = sym_size &StorageContract; + v18.i256 = sym_addr &StorageContract; + v19.i256 = sym_size &StorageContract; + evm_code_copy 0.i256 v18 v19; + evm_return 0.i256 v17; +} + +func public %solcore_entry_12_object0_2einit() { + block0: + call %solcore_fn_12_object0_2einit_37_main_5fStorageContract_5f_5fstart_5fd31013ee5; + return; +} + +func private %solcore_fn_19_object0_2einner0_2einit_35_main_5fStorageContract_5fmain_5fdd00fb0b6() -> i256 { + block0: + v1.i256 = call %solcore_fn_19_object0_2einner0_2einit_37_main_5fStorageContract_5fupdate_5fd2581ed00 42.i256; + return v1; +} + +func private %solcore_fn_19_object0_2einner0_2einit_37_main_5fStorageContract_5fupdate_5fd2581ed00(v0.i256) -> i256 { + block0: + evm_sstore 0.i256 v0; + v2.i256 = evm_sload 0.i256; + return v2; +} + +func public %solcore_entry_19_object0_2einner0_2einit() { + block0: + v0.i256 = call %solcore_fn_19_object0_2einner0_2einit_35_main_5fStorageContract_5fmain_5fdd00fb0b6; + return; +} + + +object @StorageContractDeploy { + section init { + entry %solcore_entry_12_object0_2einit; + embed .runtime as &StorageContract; + } + section runtime { + entry %solcore_entry_19_object0_2einner0_2einit; + } +} diff --git a/crates/specialize/Cargo.toml b/crates/specialize/Cargo.toml new file mode 100644 index 00000000..55d70743 --- /dev/null +++ b/crates/specialize/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "solcore-specialize" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +hir = { workspace = true } +hir-ty = { workspace = true } +nameres = { workspace = true } +parser = { workspace = true } +rustc-hash = { workspace = true } + +[dev-dependencies] +salsa = { workspace = true } +url = { workspace = true } diff --git a/crates/specialize/src/evaluate/assigned.rs b/crates/specialize/src/evaluate/assigned.rs new file mode 100644 index 00000000..43aa2b80 --- /dev/null +++ b/crates/specialize/src/evaluate/assigned.rs @@ -0,0 +1,61 @@ +use rustc_hash::FxHashSet; + +use super::{CEnv, VEnv, known::collect_pat_binders}; +use crate::ir::MonoPat; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum AssignedNames { + Names(FxHashSet), + All, +} + +impl AssignedNames { + pub(super) fn empty() -> Self { + AssignedNames::Names(FxHashSet::default()) + } + + pub(super) fn is_empty(&self) -> bool { + matches!(self, AssignedNames::Names(names) if names.is_empty()) + } + + pub(super) fn insert(&mut self, name: String) { + if let AssignedNames::Names(names) = self { + names.insert(name); + } + } + + pub(super) fn merge(&mut self, other: AssignedNames) { + match (self, other) { + (this @ AssignedNames::Names(_), AssignedNames::All) => *this = AssignedNames::All, + (AssignedNames::All, _) => {} + (AssignedNames::Names(lhs), AssignedNames::Names(rhs)) => lhs.extend(rhs), + } + } + + pub(super) fn insert_pat_binders(&mut self, pats: &[MonoPat<'_>]) { + if let AssignedNames::Names(names) = self { + for pat in pats { + collect_pat_binders(pat, names); + } + } + } +} + +pub(super) fn invalidate_assigned<'db>( + names: &AssignedNames, + env: &mut VEnv<'db>, + comptime_env: &mut CEnv, +) { + match names { + AssignedNames::All => { + env.clear(); + comptime_env.clear(); + } + AssignedNames::Names(names) => { + for name in names { + env.remove(name); + comptime_env.remove(name); + } + } + } +} diff --git a/crates/specialize/src/evaluate/core.rs b/crates/specialize/src/evaluate/core.rs new file mode 100644 index 00000000..72fee942 --- /dev/null +++ b/crates/specialize/src/evaluate/core.rs @@ -0,0 +1,2182 @@ +use std::{cmp::Ordering, collections::BTreeMap}; + +use hir::{ + ast::function::{ + AssignOp, BinOp, UnOp, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind, + }, + span::Span, +}; +use hir_ty::{BuiltinTyCtor, ConversionKind, Db, TyKind}; +use nameres::{LibraryId, module_key_for_path}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{ + CEnv, TypeReg, VEnv, YulState, + assigned::{AssignedNames, invalidate_assigned}, + effects::{ + compute_pure_funs, compute_write_effects, expr_write_effects_from_call_summaries, + intrinsic_is_pure, storage_field_names, + }, + erasure::{ + display_backend_symbol, display_mono_function_name, lambda_ret_is_comptime, + param_is_comptime, ty_is_builtin, ty_is_comptime, ty_is_function, + }, + ident_text, + known::{ + bool_expr, build_type_reg, int_expr, known_bool, known_int, known_string, + literal_from_known_expr, lvalue_root_name, match_arms_with, match_expr_arms_with, + remove_assigned, remove_comptime_assigned, string_expr, + }, + value::{ + BigInt, bitand_word, bitor_word, bitxor_word, shl_word, shr_word, word_div, word_low_byte, + word_mod, + }, + yul_const::{ + eval_yul_op, merge_yul_state, subst_yul_block, venv_to_yul_state, venv_to_yul_subst, + yul_written_names, + }, +}; +use crate::{ + ir::{ + MonoArm, MonoCallOrigin, MonoExpr, MonoExprArm, MonoExprKind, MonoFunction, MonoId, + MonoIntrinsic, MonoItem, MonoModule, MonoPat, MonoPatKind, MonoStmt, MonoStmtKind, MonoTy, + visit::{Visitor, walk_stmt}, + }, + specialize::{SpecializeDiagnostic, SpecializeDiagnosticKind}, +}; + +enum FoldOutcome<'db> { + ReturnedKnown(MonoExpr<'db>), + ReturnedUnknownAbort, + FellThroughContinue(VEnv<'db>, CEnv), +} + +struct InlineFrame<'db> { + name: String, + args: Vec>, + comptime: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum InlineBudgetExhaustion { + TotalWork { limit: usize }, + InlineDepth { limit: usize }, +} + +impl InlineBudgetExhaustion { + fn diagnostic_limit(self) -> usize { + match self { + Self::TotalWork { limit } | Self::InlineDepth { limit } => limit, + } + } +} + +fn classify_inline_budget_exhaustion( + remaining_fuel: usize, + fuel_limit: usize, + inline_depth: usize, + inline_depth_limit: usize, +) -> Option { + if remaining_fuel == 0 { + return Some(InlineBudgetExhaustion::TotalWork { limit: fuel_limit }); + } + if inline_depth >= inline_depth_limit { + return Some(InlineBudgetExhaustion::InlineDepth { + limit: inline_depth_limit, + }); + } + None +} + +pub(super) struct Evaluator<'db> { + pub(super) db: &'db dyn Db, + functions: FxHashMap>, + pure_funs: FxHashSet, + write_effects: FxHashMap, + pub(super) diagnostics: Vec>, + inline_stack: Vec>, + fuel_limit: usize, + fuel: usize, + inline_depth_limit: usize, + memory: BTreeMap, + comptime_mode: bool, + enforce_comptime: bool, +} + +struct StmtWriteEffectsCollector<'effects> { + call_effects: &'effects FxHashMap, + effects: AssignedNames, +} + +impl<'effects, 'db> Visitor<'db> for StmtWriteEffectsCollector<'effects> { + fn visit_stmt(&mut self, stmt: &MonoStmt<'db>) { + match &stmt.kind { + MonoStmtKind::Assign { lhs, .. } => { + if let Some(name) = lvalue_root_name(lhs) { + self.effects.insert(name); + } else { + self.effects.merge(AssignedNames::All); + } + } + MonoStmtKind::Assembly(_) => { + self.effects.merge(AssignedNames::All); + } + _ => {} + } + walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + self.effects.merge(expr_write_effects_from_call_summaries( + expr, + self.call_effects, + )); + } + + fn visit_pat(&mut self, _pat: &MonoPat<'db>) {} +} + +impl<'db> Evaluator<'db> { + pub(super) fn new( + db: &'db dyn Db, + module: &MonoModule<'db>, + fuel: usize, + inline_depth_limit: usize, + ) -> Self { + let functions = module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => Some((function.name.clone(), function.clone())), + _ => None, + }) + .collect::>(); + let storage_fields = storage_field_names(db, module); + let pure_funs = compute_pure_funs(db, &functions, &storage_fields); + let write_effects = compute_write_effects(&functions, &storage_fields); + Self { + db, + functions, + pure_funs, + write_effects, + diagnostics: Vec::new(), + inline_stack: Vec::new(), + fuel_limit: fuel, + fuel, + inline_depth_limit, + memory: BTreeMap::new(), + comptime_mode: false, + enforce_comptime: true, + } + } + + pub(super) fn eval_function(&mut self, mut function: MonoFunction<'db>) -> MonoFunction<'db> { + // Bound total unfolding work for each emitted function. The counter is + // monotone while that function is evaluated, so sibling calls cannot + // repeatedly reclaim the same budget. + self.fuel = self.fuel_limit; + self.memory.clear(); + let type_reg = build_type_reg(&function.params, &function.body); + let ret_comptime = ty_is_comptime(self.db, function.ret.ty()); + let comptime_env = function + .params + .iter() + .filter(|param| ret_comptime || param_is_comptime(self.db, param)) + .map(|param| param.name.clone()) + .collect::(); + let (_, _, body) = self.eval_stmts( + &type_reg, + VEnv::default(), + comptime_env, + function.body, + ret_comptime, + ); + function.body = body; + self.functions + .insert(function.name.clone(), function.clone()); + function + } + + fn expr_is_known_value(&self, expr: &MonoExpr<'db>) -> bool { + match &expr.kind { + MonoExprKind::Lit(_) | MonoExprKind::Proxy(_) | MonoExprKind::Lambda { .. } => true, + MonoExprKind::Var(id) => self.functions.contains_key(&id.name), + MonoExprKind::Tuple(elems) => elems.iter().all(|expr| self.expr_is_known_value(expr)), + MonoExprKind::Con { args, .. } => { + args.iter().all(|expr| self.expr_is_known_value(expr)) + } + MonoExprKind::Conversion { expr, .. } => self.expr_is_known_value(expr), + _ => false, + } + } + + fn eval_stmts( + &mut self, + type_reg: &TypeReg<'db>, + mut env: VEnv<'db>, + mut comptime_env: CEnv, + stmts: Vec>, + ret_comptime: bool, + ) -> (VEnv<'db>, CEnv, Vec>) { + let mut out = Vec::new(); + for stmt in stmts { + let (next_env, next_comptime_env, mut stmts) = + self.eval_stmt(type_reg, env, comptime_env, stmt, ret_comptime); + env = next_env; + comptime_env = next_comptime_env; + out.append(&mut stmts); + } + (env, comptime_env, out) + } + + fn eval_stmt( + &mut self, + type_reg: &TypeReg<'db>, + env: VEnv<'db>, + comptime_env: CEnv, + stmt: MonoStmt<'db>, + ret_comptime: bool, + ) -> (VEnv<'db>, CEnv, Vec>) { + let span = stmt.span; + match stmt.kind { + MonoStmtKind::Let { mode, id, ty, init } => { + let comptime = mode.is_comptime(); + let (init, init_effects) = match init { + Some(expr) if comptime => { + let (expr, effects) = self.with_comptime_mode(|this| { + this.eval_expr_stable(&env, &comptime_env, expr) + }); + (Some(expr), effects) + } + Some(expr) => { + let (expr, effects) = self.eval_expr_stable(&env, &comptime_env, expr); + (Some(expr), effects) + } + None => (None, AssignedNames::empty()), + }; + let mut env = env; + let mut comptime_env = comptime_env; + self.invalidate_assigned_effects(&init_effects, &mut env, &mut comptime_env); + if let Some(expr) = init.as_ref().filter(|expr| self.expr_is_known_value(expr)) { + env.insert(id.name.clone(), expr.clone()); + } else { + env.remove(&id.name); + } + let init_is_comptime = init + .as_ref() + .is_some_and(|expr| self.expr_is_comptime(expr, &comptime_env)); + if comptime || init_is_comptime { + comptime_env.insert(id.name.clone()); + } else { + comptime_env.remove(&id.name); + } + if self.enforce_comptime && comptime { + match init.as_ref() { + Some(expr) if self.expr_is_comptime(expr, &comptime_env) => { + if self.expr_is_known_value(expr) { + return (env, comptime_env, Vec::new()); + } + } + Some(_) => self.comptime_failed( + format!( + "comptime let '{}' is bound to a runtime expression", + id.name + ), + Some(span), + ), + None => self.comptime_failed( + format!("comptime let '{}' has no initializer", id.name), + Some(span), + ), + } + } + if ty_is_function(self.db, id.ty.ty()) + && init + .as_ref() + .is_some_and(|expr| self.expr_is_known_value(expr)) + { + return (env, comptime_env, Vec::new()); + } + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Let { mode, id, ty, init }, + }], + ) + } + MonoStmtKind::Return(expr) => { + let expr = expr.map(|expr| self.eval_expr_stable(&env, &comptime_env, expr).0); + if let Some(MonoExpr { + kind: MonoExprKind::Call { callee, args, .. }, + ty, + .. + }) = &expr + && self.ty_is_unit(ty.ty()) + && let Some(mut body) = self.try_inline_stmt_call(callee, args, span) + { + body.push(MonoStmt { + span, + kind: MonoStmtKind::Return(Some(MonoExpr { + span, + ty: *ty, + kind: MonoExprKind::Tuple(Vec::new()), + })), + }); + return ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Block(body), + }], + ); + } + if self.enforce_comptime + && ret_comptime + && let Some(expr) = &expr + && !self.expr_is_comptime(expr, &comptime_env) + { + self.comptime_failed( + "function with a comptime return type returns a runtime expression", + Some(span), + ); + } + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Return(expr), + }], + ) + } + MonoStmtKind::Expr(expr) => { + let (expr, effects) = self.eval_expr_stable(&env, &comptime_env, expr); + let mut env = env; + let mut comptime_env = comptime_env; + if let MonoExprKind::Call { callee, args, .. } = &expr.kind + && let Some(body) = self.try_inline_stmt_call(callee, args, span) + { + let effects = self.stmts_write_effects(&body); + self.invalidate_assigned_effects(&effects, &mut env, &mut comptime_env); + return ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Block(body), + }], + ); + } + self.invalidate_assigned_effects(&effects, &mut env, &mut comptime_env); + if self.expr_is_known_value(&expr) { + (env, comptime_env, Vec::new()) + } else { + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Expr(expr), + }], + ) + } + } + MonoStmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs, + } => { + let (lhs, target) = self.eval_lvalue(&env, &comptime_env, lhs); + let lhs_effects = self.expr_write_effects(&lhs); + let rhs_env = remove_assigned(env.clone(), &lhs_effects); + let rhs_comptime_env = remove_comptime_assigned(comptime_env.clone(), &lhs_effects); + let (rhs, rhs_effects) = self.eval_expr_stable(&rhs_env, &rhs_comptime_env, rhs); + let mut env = env; + let mut comptime_env = comptime_env; + let mut effects = lhs_effects; + effects.merge(rhs_effects); + self.invalidate_assigned_effects(&effects, &mut env, &mut comptime_env); + if let Some(id) = target { + let rhs_is_comptime = self.expr_is_comptime(&rhs, &comptime_env); + if self.expr_is_known_value(&rhs) { + if matches!(&lhs.kind, MonoExprKind::Var(_)) { + env.insert(id.name.clone(), rhs.clone()); + if rhs_is_comptime { + comptime_env.insert(id.name); + } else { + comptime_env.remove(&id.name); + } + } else { + env.remove(&id.name); + comptime_env.remove(&id.name); + } + } else { + env.remove(&id.name); + if rhs_is_comptime && matches!(&lhs.kind, MonoExprKind::Var(_)) { + comptime_env.insert(id.name); + } else { + comptime_env.remove(&id.name); + } + } + } + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs, + }, + }], + ) + } + MonoStmtKind::Assign { + op: + op @ (AssignOp::Add + | AssignOp::Sub + | AssignOp::BitXor + | AssignOp::BitAnd + | AssignOp::BitOr + | AssignOp::Mod), + lhs, + rhs, + } => self.eval_compound_assign(env, comptime_env, span, lhs, rhs, |lhs, rhs| { + MonoStmtKind::Assign { op, lhs, rhs } + }), + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + let (cond, cond_effects) = self.eval_expr_stable(&env, &comptime_env, cond); + let mut env = env; + let mut comptime_env = comptime_env; + self.invalidate_assigned_effects(&cond_effects, &mut env, &mut comptime_env); + if let Some(value) = known_bool(self.db, &cond) { + let selected = if value { + then_body + } else { + else_body.unwrap_or_default() + }; + return self.eval_stmts(type_reg, env, comptime_env, selected, ret_comptime); + } + let mut assigned = self.stmts_write_effects(&then_body); + if let Some(else_body) = else_body.as_deref() { + assigned.merge(self.stmts_write_effects(else_body)); + } + let (branch_env, branch_comptime_env) = + self.mask_assigned_env(env.clone(), comptime_env.clone(), &assigned); + let (_, _, then_body) = self.eval_stmts( + type_reg, + branch_env.clone(), + branch_comptime_env.clone(), + then_body, + ret_comptime, + ); + let else_body = else_body.map(|body| { + let (_, _, body) = self.eval_stmts( + type_reg, + branch_env.clone(), + branch_comptime_env.clone(), + body, + ret_comptime, + ); + body + }); + let (env, comptime_env) = self.mask_assigned_env(env, comptime_env, &assigned); + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::If { + cond, + then_body, + else_body, + }, + }], + ) + } + MonoStmtKind::Match { scrutinees, arms } => { + let mut env = env; + let mut comptime_env = comptime_env; + let raw_scrutinees = scrutinees; + let mut scrutinees = Vec::with_capacity(raw_scrutinees.len()); + for scrutinee in raw_scrutinees { + let (scrutinee, effects) = + self.eval_expr_stable(&env, &comptime_env, scrutinee); + self.invalidate_assigned_effects(&effects, &mut env, &mut comptime_env); + scrutinees.push(scrutinee); + } + let arms = arms + .into_iter() + .map(|arm| self.eval_arm_labels(&env, &comptime_env, arm)) + .collect::>(); + if scrutinees.iter().all(|expr| self.expr_is_known_value(expr)) { + let matched = match_arms_with(&env, &scrutinees, &arms, |expr| { + self.expr_is_known_value(expr) + }); + if let Some((matched_env, body)) = matched { + let matched_comptime_env = + self.with_known_env_bindings_comptime(&matched_env, comptime_env); + return self.eval_stmts( + type_reg, + matched_env, + matched_comptime_env, + body, + ret_comptime, + ); + } + } + let mut assigned = AssignedNames::empty(); + for arm in &arms { + assigned.merge(self.stmts_write_effects(&arm.body)); + } + let arms = arms + .into_iter() + .map(|arm| { + let mut masked = self.stmts_write_effects(&arm.body); + masked.insert_pat_binders(&arm.pats); + let (arm_env, arm_comptime_env) = + self.mask_assigned_env(env.clone(), comptime_env.clone(), &masked); + let (_, _, body) = self.eval_stmts( + type_reg, + arm_env, + arm_comptime_env, + arm.body, + ret_comptime, + ); + MonoArm { body, ..arm } + }) + .collect::>(); + let (env, comptime_env) = self.mask_assigned_env(env, comptime_env, &assigned); + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Match { scrutinees, arms }, + }], + ) + } + MonoStmtKind::Block(body) => { + let assigned = self.stmts_write_effects(&body); + let (_, _, body) = self.eval_stmts( + type_reg, + env.clone(), + comptime_env.clone(), + body, + ret_comptime, + ); + let (env, comptime_env) = self.mask_assigned_env(env, comptime_env, &assigned); + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Block(body), + }], + ) + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + // Names written anywhere in the loop (init/cond/post/body) + // must not fold to their pre-loop constants. + let mut assigned = self.stmts_write_effects(&body); + assigned.merge(self.stmts_write_effects(&init)); + assigned.merge(self.expr_write_effects(&cond)); + assigned.merge(self.stmts_write_effects(&post)); + let (loop_env, loop_comptime_env) = + self.mask_assigned_env(env.clone(), comptime_env.clone(), &assigned); + let (_, _, init) = self.eval_stmts( + type_reg, + loop_env.clone(), + loop_comptime_env.clone(), + init, + ret_comptime, + ); + let cond = self.eval_expr(&loop_env, &loop_comptime_env, cond); + let (_, _, post) = self.eval_stmts( + type_reg, + loop_env.clone(), + loop_comptime_env.clone(), + post, + ret_comptime, + ); + let (_, _, body) = + self.eval_stmts(type_reg, loop_env, loop_comptime_env, body, ret_comptime); + ( + VEnv::default(), + CEnv::default(), + vec![MonoStmt { + span, + kind: MonoStmtKind::For { + init, + cond, + post, + body, + }, + }], + ) + } + MonoStmtKind::Assembly(body) => { + let mut subst = venv_to_yul_subst(self.db, &env); + let written = yul_written_names(self.db, &body); + subst.retain(|name, _| !written.contains(name)); + let body = subst_yul_block(self.db, &subst, body); + let state = venv_to_yul_state(&env); + if let Some(state) = self.eval_yul_block(state, &body) { + ( + merge_yul_state(type_reg, state, env), + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Assembly(body), + }], + ) + } else { + let (env, comptime_env) = self.preserve_comptime_known_env(env, comptime_env); + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Assembly(body), + }], + ) + } + } + MonoStmtKind::Break => ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Break, + }], + ), + MonoStmtKind::Continue => ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Continue, + }], + ), + MonoStmtKind::Error => ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: MonoStmtKind::Error, + }], + ), + } + } + + fn eval_compound_assign( + &mut self, + env: VEnv<'db>, + comptime_env: CEnv, + span: Span<'db>, + lhs: MonoExpr<'db>, + rhs: MonoExpr<'db>, + make_kind: impl FnOnce(MonoExpr<'db>, MonoExpr<'db>) -> MonoStmtKind<'db>, + ) -> (VEnv<'db>, CEnv, Vec>) { + let (lhs, target) = self.eval_lvalue(&env, &comptime_env, lhs); + let lhs_effects = self.expr_write_effects(&lhs); + let rhs_env = remove_assigned(env.clone(), &lhs_effects); + let rhs_comptime_env = remove_comptime_assigned(comptime_env.clone(), &lhs_effects); + let (rhs, rhs_effects) = self.eval_expr_stable(&rhs_env, &rhs_comptime_env, rhs); + let mut env = env; + let mut comptime_env = comptime_env; + let mut effects = lhs_effects; + effects.merge(rhs_effects); + self.invalidate_assigned_effects(&effects, &mut env, &mut comptime_env); + if let Some(id) = target { + env.remove(&id.name); + comptime_env.remove(&id.name); + } + ( + env, + comptime_env, + vec![MonoStmt { + span, + kind: make_kind(lhs, rhs), + }], + ) + } + + fn with_known_env_bindings_comptime(&self, env: &VEnv<'db>, mut comptime_env: CEnv) -> CEnv { + for (name, expr) in env { + if self.expr_is_known_value(expr) { + comptime_env.insert(name.clone()); + } + } + comptime_env + } + + fn preserve_comptime_known_env(&self, env: VEnv<'db>, comptime_env: CEnv) -> (VEnv<'db>, CEnv) { + let mut kept_env = VEnv::default(); + let mut kept_comptime_env = CEnv::default(); + for (name, expr) in env { + if comptime_env.contains(&name) && self.expr_survives_unknown_write(&expr) { + kept_comptime_env.insert(name.clone()); + kept_env.insert(name, expr); + } + } + (kept_env, kept_comptime_env) + } + + fn expr_survives_unknown_write(&self, expr: &MonoExpr<'db>) -> bool { + match &expr.kind { + MonoExprKind::Proxy(_) | MonoExprKind::Lambda { .. } => true, + MonoExprKind::Var(id) => self.functions.contains_key(&id.name), + MonoExprKind::Tuple(elems) => elems + .iter() + .all(|expr| self.expr_survives_unknown_write(expr)), + MonoExprKind::Con { args, .. } => args + .iter() + .all(|expr| self.expr_survives_unknown_write(expr)), + MonoExprKind::Conversion { expr, .. } => self.expr_survives_unknown_write(expr), + _ => false, + } + } + + fn mask_assigned_env( + &self, + env: VEnv<'db>, + comptime_env: CEnv, + assigned: &AssignedNames, + ) -> (VEnv<'db>, CEnv) { + match assigned { + AssignedNames::All => self.preserve_comptime_known_env(env, comptime_env), + AssignedNames::Names(_) => ( + remove_assigned(env, assigned), + remove_comptime_assigned(comptime_env, assigned), + ), + } + } + + fn invalidate_assigned_effects( + &self, + assigned: &AssignedNames, + env: &mut VEnv<'db>, + comptime_env: &mut CEnv, + ) { + if matches!(assigned, AssignedNames::All) { + let old_env = std::mem::take(env); + let old_comptime_env = std::mem::take(comptime_env); + let (kept_env, kept_comptime_env) = + self.preserve_comptime_known_env(old_env, old_comptime_env); + *env = kept_env; + *comptime_env = kept_comptime_env; + } else { + invalidate_assigned(assigned, env, comptime_env); + } + } + + fn eval_lvalue( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + expr: MonoExpr<'db>, + ) -> (MonoExpr<'db>, Option>) { + let span = expr.span; + let ty = expr.ty; + match expr.kind { + MonoExprKind::Var(id) => ( + MonoExpr { + span, + ty, + kind: MonoExprKind::Var(id.clone()), + }, + Some(id), + ), + MonoExprKind::Index { base, index } => { + let (base, target) = self.eval_lvalue(env, comptime_env, *base); + let index = self.eval_expr(env, comptime_env, *index); + ( + MonoExpr { + span, + ty, + kind: MonoExprKind::Index { + base: Box::new(base), + index: Box::new(index), + }, + }, + target, + ) + } + MonoExprKind::StorageIndex { base, index } => { + let (base, target) = self.eval_lvalue(env, comptime_env, *base); + let index = self.eval_expr(env, comptime_env, *index); + ( + MonoExpr { + span, + ty, + kind: MonoExprKind::StorageIndex { + base: Box::new(base), + index: Box::new(index), + }, + }, + target, + ) + } + MonoExprKind::Field { base, field } => { + let (base, target) = self.eval_lvalue(env, comptime_env, *base); + ( + MonoExpr { + span, + ty, + kind: MonoExprKind::Field { + base: Box::new(base), + field, + }, + }, + target, + ) + } + kind => (MonoExpr { span, ty, kind }, None), + } + } + + fn eval_expr( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + expr: MonoExpr<'db>, + ) -> MonoExpr<'db> { + let span = expr.span; + let ty = expr.ty; + match expr.kind { + MonoExprKind::Var(id) => env.get(&id.name).cloned().unwrap_or(MonoExpr { + span, + ty, + kind: MonoExprKind::Var(id), + }), + MonoExprKind::Lit(_) | MonoExprKind::Error => MonoExpr { + span, + ty, + kind: expr.kind, + }, + MonoExprKind::Lambda { name, params, body } => { + let type_reg = build_type_reg(¶ms, &body); + let ret_comptime = lambda_ret_is_comptime(self.db, ty.ty()); + let (_, _, body) = self.eval_stmts( + &type_reg, + env.clone(), + comptime_env.clone(), + body, + ret_comptime, + ); + MonoExpr { + span, + ty, + kind: MonoExprKind::Lambda { name, params, body }, + } + } + MonoExprKind::Tuple(elems) => MonoExpr { + span, + ty, + kind: MonoExprKind::Tuple( + elems + .into_iter() + .map(|expr| self.eval_expr(env, comptime_env, expr)) + .collect(), + ), + }, + MonoExprKind::Call { + callee, + args, + origin, + } => { + let args = args + .into_iter() + .map(|arg| self.eval_expr(env, comptime_env, arg)) + .collect::>(); + if let MonoCallOrigin::Builtin(intrinsic) = origin + && let Some(result) = self.eval_primitive(intrinsic, &args, ty, span) + { + return result; + } + if !matches!(origin, MonoCallOrigin::Builtin(_)) { + self.check_comptime_params(&callee.name, &args, comptime_env, span); + if let Some(result) = self.try_inline(&callee.name, &args, span) { + return result; + } + } + MonoExpr { + span, + ty, + kind: MonoExprKind::Call { + callee, + args, + origin, + }, + } + } + MonoExprKind::Con { ctor, args } => MonoExpr { + span, + ty, + kind: MonoExprKind::Con { + ctor, + args: args + .into_iter() + .map(|arg| self.eval_expr(env, comptime_env, arg)) + .collect(), + }, + }, + MonoExprKind::ClosureDispatch { callee, args } => { + let callee = self.eval_expr(env, comptime_env, *callee); + let args = args + .into_iter() + .map(|arg| self.eval_expr(env, comptime_env, arg)) + .collect::>(); + if let Some(result) = self.eval_closure_dispatch(&callee, &args, ty, span) { + return result; + } + MonoExpr { + span, + ty, + kind: MonoExprKind::ClosureDispatch { + callee: Box::new(callee), + args, + }, + } + } + MonoExprKind::BinOp { lhs, op, rhs } => { + let lhs = self.eval_expr(env, comptime_env, *lhs); + if matches!(op, BinOp::And | BinOp::Or) { + return match (op, known_bool(self.db, &lhs)) { + (BinOp::And, Some(false)) => bool_expr(false, ty, span), + (BinOp::Or, Some(true)) => bool_expr(true, ty, span), + (BinOp::And, Some(true)) | (BinOp::Or, Some(false)) => { + self.eval_expr(env, comptime_env, *rhs) + } + _ => MonoExpr { + span, + ty, + kind: MonoExprKind::BinOp { + lhs: Box::new(lhs), + op, + rhs, + }, + }, + }; + } + let rhs = self.eval_expr(env, comptime_env, *rhs); + if let Some(result) = self.eval_binop(&lhs, op, &rhs, ty, span) { + return result; + } + MonoExpr { + span, + ty, + kind: MonoExprKind::BinOp { + lhs: Box::new(lhs), + op, + rhs: Box::new(rhs), + }, + } + } + MonoExprKind::UnaryOp { op, expr } => { + let expr = self.eval_expr(env, comptime_env, *expr); + if let Some(result) = self.eval_unary(op, &expr, ty, span) { + return result; + } + MonoExpr { + span, + ty, + kind: MonoExprKind::UnaryOp { + op, + expr: Box::new(expr), + }, + } + } + MonoExprKind::Index { base, index } => MonoExpr { + span, + ty, + kind: MonoExprKind::Index { + base: Box::new(self.eval_expr(env, comptime_env, *base)), + index: Box::new(self.eval_expr(env, comptime_env, *index)), + }, + }, + MonoExprKind::StorageIndex { base, index } => MonoExpr { + span, + ty, + kind: MonoExprKind::StorageIndex { + base: Box::new(self.eval_expr(env, comptime_env, *base)), + index: Box::new(self.eval_expr(env, comptime_env, *index)), + }, + }, + MonoExprKind::Field { base, field } => { + let base = self.eval_expr(env, comptime_env, *base); + if let Ok(index) = field.parse::() + && let MonoExprKind::Tuple(elems) = &base.kind + && let Some(elem) = elems.get(index) + { + return elem.clone(); + } + MonoExpr { + span, + ty, + kind: MonoExprKind::Field { + base: Box::new(base), + field, + }, + } + } + MonoExprKind::Proxy(proxy_ty) => MonoExpr { + span, + ty, + kind: MonoExprKind::Proxy(proxy_ty), + }, + MonoExprKind::Conversion { + expr, + ty: annot_ty, + kind: + kind @ (ConversionKind::Identity + | ConversionKind::ValueTypeWrap + | ConversionKind::ValueTypeUnwrap), + } => { + let expr = self.eval_expr(env, comptime_env, *expr); + if self.expr_is_known_value(&expr) { + MonoExpr { + span, + ty, + kind: expr.kind, + } + } else { + MonoExpr { + span, + ty, + kind: MonoExprKind::Conversion { + expr: Box::new(expr), + ty: annot_ty, + kind, + }, + } + } + } + MonoExprKind::Match { scrutinee, arms } => { + let scrutinee = self.eval_expr(env, comptime_env, *scrutinee); + let arms = arms + .into_iter() + .map(|arm| self.eval_expr_arm_labels(env, comptime_env, arm)) + .collect::>(); + if self.expr_is_known_value(&scrutinee) + && let Some((matched_env, expr)) = + match_expr_arms_with(env, &scrutinee, &arms, |expr| { + self.expr_is_known_value(expr) + }) + { + let matched_comptime_env = + self.with_known_env_bindings_comptime(&matched_env, comptime_env.clone()); + return self.eval_expr(&matched_env, &matched_comptime_env, expr); + } + MonoExpr { + span, + ty, + kind: MonoExprKind::Match { + scrutinee: Box::new(scrutinee), + arms: arms + .into_iter() + .map(|arm| MonoExprArm { + expr: self.eval_expr(env, comptime_env, arm.expr), + ..arm + }) + .collect(), + }, + } + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => { + let cond = self.eval_expr(env, comptime_env, *cond); + if let Some(value) = known_bool(self.db, &cond) { + return if value { + self.eval_expr(env, comptime_env, *then_expr) + } else { + self.eval_expr(env, comptime_env, *else_expr) + }; + } + MonoExpr { + span, + ty, + kind: MonoExprKind::If { + cond: Box::new(cond), + then_expr: Box::new(self.eval_expr(env, comptime_env, *then_expr)), + else_expr: Box::new(self.eval_expr(env, comptime_env, *else_expr)), + }, + } + } + } + } + + fn eval_expr_stable( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + expr: MonoExpr<'db>, + ) -> (MonoExpr<'db>, AssignedNames) { + let evaluated = self.eval_expr(env, comptime_env, expr.clone()); + let effects = self.expr_write_effects(&evaluated); + if effects.is_empty() { + return (evaluated, effects); + } + let (masked_env, masked_comptime_env) = + self.mask_assigned_env(env.clone(), comptime_env.clone(), &effects); + let evaluated = self.eval_expr(&masked_env, &masked_comptime_env, expr); + let effects = self.expr_write_effects(&evaluated); + (evaluated, effects) + } + + fn expr_write_effects(&self, expr: &MonoExpr<'db>) -> AssignedNames { + expr_write_effects_from_call_summaries(expr, &self.write_effects) + } + + fn stmts_write_effects(&self, stmts: &[MonoStmt<'db>]) -> AssignedNames { + let mut collector = StmtWriteEffectsCollector { + call_effects: &self.write_effects, + effects: AssignedNames::empty(), + }; + for stmt in stmts { + collector.visit_stmt(stmt); + } + collector.effects + } + + fn eval_closure_dispatch( + &mut self, + callee: &MonoExpr<'db>, + args: &[MonoExpr<'db>], + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + match &callee.kind { + MonoExprKind::Var(id) if self.functions.contains_key(&id.name) => { + let function = self.functions.get(&id.name)?; + let args = self.closure_call_args(function, args); + self.check_comptime_params(&id.name, &args, &CEnv::default(), span); + self.try_inline(&id.name, &args, span).or_else(|| { + Some(MonoExpr { + span, + ty, + kind: MonoExprKind::Call { + callee: id.clone(), + args, + origin: MonoCallOrigin::ByName, + }, + }) + }) + } + MonoExprKind::Lambda { name, params, body } if params.len() == args.len() => { + let ret_comptime = lambda_ret_is_comptime(self.db, ty.ty()); + let frame_comptime = self.comptime_mode + || ret_comptime + || params.iter().any(|param| param_is_comptime(self.db, param)); + let frame_name = format!( + "lambda:{}:{}:{}", + name, + span.begin().as_u32(), + span.end().as_u32() + ); + if self.has_recursive_inline_frame(&frame_name, args) { + self.push_recursion_diagnostic(name.clone(), frame_comptime, None, span); + return None; + } + if let Some(exhaustion) = self.inline_budget_exhaustion() { + self.push_inline_limit_diagnostic( + name.clone(), + self.inline_chain_is_comptime(frame_comptime), + span, + exhaustion, + ); + return None; + } + self.fuel -= 1; + self.inline_stack.push(InlineFrame { + name: frame_name, + args: args.to_vec(), + comptime: frame_comptime, + }); + let mut env = VEnv::default(); + let mut comptime_env = CEnv::default(); + for (param, arg) in params.iter().zip(args) { + if self.expr_is_known_value(arg) { + env.insert(param.name.clone(), arg.clone()); + comptime_env.insert(param.name.clone()); + } else if param_is_comptime(self.db, param) { + comptime_env.insert(param.name.clone()); + } + } + let type_reg = build_type_reg(params, body); + let result = self.eval_fun_body(&type_reg, env, comptime_env, body.clone()); + let frame = self.inline_stack.pop(); + debug_assert!(frame.is_some_and(|frame| frame.name.starts_with("lambda:"))); + match result { + FoldOutcome::ReturnedKnown(expr) => Some(expr), + FoldOutcome::ReturnedUnknownAbort | FoldOutcome::FellThroughContinue(_, _) => { + None + } + } + } + MonoExprKind::Conversion { expr, .. } => { + self.eval_closure_dispatch(expr, args, ty, span) + } + _ => None, + } + } + + fn closure_call_args( + &self, + function: &MonoFunction<'db>, + args: &[MonoExpr<'db>], + ) -> Vec> { + if args.len() == 1 && function.params.is_empty() && self.ty_is_unit(args[0].ty.ty()) { + return Vec::new(); + } + if args.len() == 1 + && function.params.len() != 1 + && let MonoExprKind::Tuple(elems) = &args[0].kind + && elems.len() == function.params.len() + { + return elems.clone(); + } + if args.len() == 1 && function.params.len() != 1 { + return function + .params + .iter() + .enumerate() + .map(|(index, param)| MonoExpr { + span: args[0].span, + ty: param.ty, + kind: MonoExprKind::Field { + base: Box::new(args[0].clone()), + field: index.to_string(), + }, + }) + .collect(); + } + args.to_vec() + } + + fn eval_arm_labels( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + mut arm: MonoArm<'db>, + ) -> MonoArm<'db> { + arm.pats = arm + .pats + .into_iter() + .map(|pat| self.eval_pat_label(env, comptime_env, pat)) + .collect(); + arm + } + + fn eval_expr_arm_labels( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + mut arm: MonoExprArm<'db>, + ) -> MonoExprArm<'db> { + arm.pat = self.eval_pat_label(env, comptime_env, arm.pat); + arm + } + + fn eval_pat_label( + &mut self, + env: &VEnv<'db>, + comptime_env: &CEnv, + pat: MonoPat<'db>, + ) -> MonoPat<'db> { + let span = pat.span; + let ty = pat.ty; + match pat.kind { + MonoPatKind::ComptimeLabel(expr) => { + let expr = self.eval_expr(env, comptime_env, expr); + match literal_from_known_expr(&expr) { + Some(lit) => MonoPat { + span, + ty, + kind: MonoPatKind::Lit(lit), + }, + None => { + if self.enforce_comptime { + self.comptime_failed( + "comptime expression in match label could not be evaluated", + Some(span), + ); + } + MonoPat { + span, + ty, + kind: MonoPatKind::ComptimeLabel(expr), + } + } + } + } + MonoPatKind::Con { ctor, args } => MonoPat { + span, + ty, + kind: MonoPatKind::Con { + ctor, + args: args + .into_iter() + .map(|arg| self.eval_pat_label(env, comptime_env, arg)) + .collect(), + }, + }, + MonoPatKind::Tuple(elems) => MonoPat { + span, + ty, + kind: MonoPatKind::Tuple( + elems + .into_iter() + .map(|elem| self.eval_pat_label(env, comptime_env, elem)) + .collect(), + ), + }, + kind => MonoPat { span, ty, kind }, + } + } + + fn eval_primitive( + &self, + intrinsic: MonoIntrinsic, + args: &[MonoExpr<'db>], + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + match (intrinsic, args) { + (MonoIntrinsic::WordToInteger, [arg]) => { + known_int(arg).map(|value| int_expr(value, ty, span)) + } + (MonoIntrinsic::WordFromInteger, [arg]) => { + known_int(arg).map(|value| int_expr(value.mod_word(), ty, span)) + } + (MonoIntrinsic::IntegerAdd, [lhs, rhs]) => { + Some(int_expr(known_int(lhs)?.add(&known_int(rhs)?), ty, span)) + } + (MonoIntrinsic::IntegerSub, [lhs, rhs]) => { + Some(int_expr(known_int(lhs)?.sub(&known_int(rhs)?), ty, span)) + } + (MonoIntrinsic::IntegerMul, [lhs, rhs]) => { + Some(int_expr(known_int(lhs)?.mul(&known_int(rhs)?), ty, span)) + } + (MonoIntrinsic::IntegerLt, [lhs, rhs]) => Some(bool_expr( + known_int(lhs)?.cmp(&known_int(rhs)?) == Ordering::Less, + ty, + span, + )), + (MonoIntrinsic::IntegerEq, [lhs, rhs]) => { + Some(bool_expr(known_int(lhs)? == known_int(rhs)?, ty, span)) + } + (MonoIntrinsic::ConcatLit, [lhs, rhs]) => Some(string_expr( + format!("{}{}", known_string(lhs)?, known_string(rhs)?), + ty, + span, + )), + (MonoIntrinsic::StrlenLit, [arg]) => { + let len = known_string(arg)?.len() as u64; + Some(int_expr(BigInt::from_u64(len), ty, span)) + } + (MonoIntrinsic::KeccakLit, [arg]) => { + let hash = hir::keccak::keccak256(known_string(arg)?.as_bytes()); + Some(int_expr(BigInt::from_be_bytes(&hash), ty, span)) + } + (MonoIntrinsic::PrimAddWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::Add, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::SubWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::Sub, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::MulWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::Mul, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::GtWord, [lhs, rhs]) => { + self.eval_word_binary(WordBinaryOp::Gt, known_int(lhs)?, known_int(rhs)?, ty, span) + } + (MonoIntrinsic::BxorWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::BitXor, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::BandWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::BitAnd, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::BorWord, [lhs, rhs]) => self.eval_word_binary( + WordBinaryOp::BitOr, + known_int(lhs)?, + known_int(rhs)?, + ty, + span, + ), + (MonoIntrinsic::PrimEqWord, [lhs, rhs]) => { + self.eval_word_binary(WordBinaryOp::Eq, known_int(lhs)?, known_int(rhs)?, ty, span) + } + _ => None, + } + } + + fn eval_binop( + &self, + lhs: &MonoExpr<'db>, + op: BinOp, + rhs: &MonoExpr<'db>, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + if op == BinOp::Add + && let (Some(lhs), Some(rhs)) = (known_string(lhs), known_string(rhs)) + { + return Some(string_expr(format!("{lhs}{rhs}"), ty, span)); + } + let lhs_int = known_int(lhs)?; + let rhs_int = known_int(rhs)?; + if ty_is_builtin(self.db, ty.ty(), BuiltinTyCtor::Integer) { + return match op { + BinOp::Add => Some(int_expr(lhs_int.add(&rhs_int), ty, span)), + BinOp::Sub => Some(int_expr(lhs_int.sub(&rhs_int), ty, span)), + BinOp::Mul => Some(int_expr(lhs_int.mul(&rhs_int), ty, span)), + BinOp::Eq => Some(bool_expr(lhs_int == rhs_int, ty, span)), + BinOp::NotEq => Some(bool_expr(lhs_int != rhs_int, ty, span)), + BinOp::Lt => Some(bool_expr(lhs_int < rhs_int, ty, span)), + BinOp::Gt => Some(bool_expr(lhs_int > rhs_int, ty, span)), + BinOp::LtEq => Some(bool_expr(lhs_int <= rhs_int, ty, span)), + BinOp::GtEq => Some(bool_expr(lhs_int >= rhs_int, ty, span)), + _ => None, + }; + } + if ty_is_builtin(self.db, ty.ty(), BuiltinTyCtor::Bool) { + return match op { + BinOp::Eq => Some(bool_expr(lhs_int == rhs_int, ty, span)), + BinOp::NotEq => Some(bool_expr(lhs_int != rhs_int, ty, span)), + BinOp::Lt => Some(bool_expr(lhs_int.mod_word() < rhs_int.mod_word(), ty, span)), + BinOp::Gt => Some(bool_expr(lhs_int.mod_word() > rhs_int.mod_word(), ty, span)), + BinOp::LtEq => Some(bool_expr( + lhs_int.mod_word() <= rhs_int.mod_word(), + ty, + span, + )), + BinOp::GtEq => Some(bool_expr( + lhs_int.mod_word() >= rhs_int.mod_word(), + ty, + span, + )), + _ => None, + }; + } + if ty_is_builtin(self.db, ty.ty(), BuiltinTyCtor::Word) { + return match op { + BinOp::Add => Some(int_expr(lhs_int.add(&rhs_int).mod_word(), ty, span)), + BinOp::Sub => Some(int_expr(lhs_int.sub(&rhs_int).mod_word(), ty, span)), + BinOp::Mul => Some(int_expr(lhs_int.mul(&rhs_int).mod_word(), ty, span)), + BinOp::Div => Some(int_expr(word_div(lhs_int, rhs_int), ty, span)), + BinOp::Mod => Some(int_expr(word_mod(lhs_int, rhs_int), ty, span)), + BinOp::BitAnd => Some(int_expr(bitand_word(&lhs_int, &rhs_int), ty, span)), + BinOp::BitOr => Some(int_expr(bitor_word(&lhs_int, &rhs_int), ty, span)), + BinOp::BitXor => Some(int_expr(bitxor_word(&lhs_int, &rhs_int), ty, span)), + BinOp::Shl => Some(int_expr(shl_word(&lhs_int, &rhs_int), ty, span)), + BinOp::Shr => Some(int_expr(shr_word(&lhs_int, &rhs_int), ty, span)), + _ => None, + }; + } + None + } + + fn eval_unary( + &self, + op: UnOp, + expr: &MonoExpr<'db>, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + match op { + UnOp::Not => known_bool(self.db, expr).map(|value| bool_expr(!value, ty, span)), + UnOp::Error => None, + } + } + + fn eval_word_binary( + &self, + op: WordBinaryOp, + lhs: BigInt, + rhs: BigInt, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + let expr = match op { + WordBinaryOp::Add => int_expr(lhs.add(&rhs).mod_word(), ty, span), + WordBinaryOp::Sub => int_expr(lhs.sub(&rhs).mod_word(), ty, span), + WordBinaryOp::Mul => int_expr(lhs.mul(&rhs).mod_word(), ty, span), + WordBinaryOp::Gt => bool_expr(lhs.mod_word() > rhs.mod_word(), ty, span), + WordBinaryOp::BitXor => int_expr(bitxor_word(&lhs, &rhs), ty, span), + WordBinaryOp::BitAnd => int_expr(bitand_word(&lhs, &rhs), ty, span), + WordBinaryOp::BitOr => int_expr(bitor_word(&lhs, &rhs), ty, span), + WordBinaryOp::Eq => bool_expr(lhs.mod_word() == rhs.mod_word(), ty, span), + }; + Some(expr) + } + + fn try_inline( + &mut self, + name: &str, + args: &[MonoExpr<'db>], + span: Span<'db>, + ) -> Option> { + let function = self.functions.get(name)?.clone(); + if !self.function_can_inline(name, &function) { + return None; + } + if function.params.len() != args.len() { + return None; + } + let ret_comptime = ty_is_comptime(self.db, function.ret.ty()); + let frame_comptime = self.comptime_mode + || ret_comptime + || function + .params + .iter() + .any(|param| param_is_comptime(self.db, param)); + let function_display = display_mono_function_name(self.db, &function); + if self.has_recursive_inline_frame(name, args) { + let shadowed = (!frame_comptime) + .then(|| function.shadowed_top_level.clone()) + .flatten(); + self.push_recursion_diagnostic(function_display, frame_comptime, shadowed, span); + return None; + } + if let Some(exhaustion) = self.inline_budget_exhaustion() { + self.push_inline_limit_diagnostic( + function_display, + self.inline_chain_is_comptime(frame_comptime), + span, + exhaustion, + ); + return None; + } + self.fuel -= 1; + self.inline_stack.push(InlineFrame { + name: name.to_owned(), + args: args.to_vec(), + comptime: frame_comptime, + }); + let mut env = VEnv::default(); + let mut comptime_env = CEnv::default(); + for (param, arg) in function.params.iter().zip(args) { + if self.expr_is_known_value(arg) { + env.insert(param.name.clone(), arg.clone()); + } + if ret_comptime || param_is_comptime(self.db, param) || self.expr_is_known_value(arg) { + comptime_env.insert(param.name.clone()); + } + } + let type_reg = build_type_reg(&function.params, &function.body); + let result = self.eval_fun_body(&type_reg, env, comptime_env, function.body); + let frame = self.inline_stack.pop(); + debug_assert!(frame.is_some_and(|frame| frame.name == name)); + match result { + FoldOutcome::ReturnedKnown(expr) => Some(expr), + FoldOutcome::ReturnedUnknownAbort | FoldOutcome::FellThroughContinue(_, _) => None, + } + } + + fn try_inline_stmt_call( + &mut self, + callee: &MonoId<'db>, + args: &[MonoExpr<'db>], + span: Span<'db>, + ) -> Option>> { + let function = self.functions.get(&callee.name)?.clone(); + if !self.function_is_std_dispatch(&function) || function.params.len() != args.len() { + return None; + } + if !args.iter().all(|arg| self.expr_is_known_value(arg)) { + return None; + } + let ret_comptime = ty_is_comptime(self.db, function.ret.ty()); + let frame_comptime = self.comptime_mode + || ret_comptime + || function + .params + .iter() + .any(|param| param_is_comptime(self.db, param)); + let function_display = display_mono_function_name(self.db, &function); + if self.has_recursive_inline_frame(&callee.name, args) { + self.push_recursion_diagnostic(function_display, frame_comptime, None, span); + return None; + } + if let Some(exhaustion) = self.inline_budget_exhaustion() { + self.push_inline_limit_diagnostic( + function_display, + self.inline_chain_is_comptime(frame_comptime), + span, + exhaustion, + ); + return None; + } + self.fuel -= 1; + self.inline_stack.push(InlineFrame { + name: callee.name.clone(), + args: args.to_vec(), + comptime: frame_comptime, + }); + let mut env = VEnv::default(); + let mut comptime_env = CEnv::default(); + for (param, arg) in function.params.iter().zip(args) { + env.insert(param.name.clone(), arg.clone()); + comptime_env.insert(param.name.clone()); + } + let type_reg = build_type_reg(&function.params, &function.body); + let (_, _, body) = self.eval_stmts(&type_reg, env, comptime_env, function.body, false); + let frame = self.inline_stack.pop(); + debug_assert!(frame.is_some_and(|frame| frame.name == callee.name)); + Some(body) + } + + fn function_can_inline(&self, name: &str, function: &MonoFunction<'db>) -> bool { + self.pure_funs.contains(name) || self.function_is_std_dispatch(function) + } + + fn function_is_std_dispatch(&self, function: &MonoFunction<'db>) -> bool { + let Some(path) = function + .source + .and_then(|def| hir::url_to_file_path(def.file(self.db).url(self.db))) + else { + return false; + }; + module_key_for_path( + LibraryId::Std, + self.db.module_tree().std_root(self.db), + &path, + ) + .is_some_and(|key| key.logical_path.as_slice() == ["dispatch"]) + } + + fn ty_is_unit(&self, ty: hir_ty::Ty<'db>) -> bool { + ty_is_builtin(self.db, ty, BuiltinTyCtor::Unit) + || matches!(ty.kind(self.db), TyKind::Tuple(elems) if elems.is_empty()) + } + + fn has_recursive_inline_frame(&self, name: &str, args: &[MonoExpr<'db>]) -> bool { + self.inline_stack + .iter() + .any(|frame| frame.name == name && frame.args == args) + } + + fn inline_chain_is_comptime(&self, current_frame_comptime: bool) -> bool { + current_frame_comptime || self.inline_stack.iter().any(|frame| frame.comptime) + } + + fn inline_budget_exhaustion(&self) -> Option { + classify_inline_budget_exhaustion( + self.fuel, + self.fuel_limit, + self.inline_stack.len(), + self.inline_depth_limit, + ) + } + + fn has_inline_failure_diagnostic(&self) -> bool { + self.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic.kind, + SpecializeDiagnosticKind::ComptimeFuelExhausted { .. } + | SpecializeDiagnosticKind::ComptimeRecursion { .. } + | SpecializeDiagnosticKind::ReductionRecursion { .. } + | SpecializeDiagnosticKind::ReductionFuelExhausted { .. } + ) + }) + } + + fn push_recursion_diagnostic( + &mut self, + function: String, + comptime: bool, + shadowed_top_level: Option, + span: Span<'db>, + ) { + if self.has_inline_failure_diagnostic() { + return; + } + let kind = if self.inline_chain_is_comptime(comptime) { + SpecializeDiagnosticKind::ComptimeRecursion { function } + } else { + SpecializeDiagnosticKind::ReductionRecursion { + function, + shadowed_top_level, + } + }; + self.diagnostics.push(SpecializeDiagnostic { + kind, + span: Some(span), + }); + } + + fn push_inline_limit_diagnostic( + &mut self, + function: String, + comptime: bool, + span: Span<'db>, + exhaustion: InlineBudgetExhaustion, + ) { + if self.has_inline_failure_diagnostic() { + return; + } + // Both limits bound evaluator unfold steps. Keep the established + // SC0410/SC0414 fuel diagnostics for compatibility, but report the + // limit that actually stopped evaluation: total work or inline depth. + let limit = exhaustion.diagnostic_limit(); + let kind = if comptime { + SpecializeDiagnosticKind::ComptimeFuelExhausted { function, limit } + } else { + SpecializeDiagnosticKind::ReductionFuelExhausted { function, limit } + }; + self.diagnostics.push(SpecializeDiagnostic { + kind, + span: Some(span), + }); + } + + fn eval_fun_body( + &mut self, + type_reg: &TypeReg<'db>, + mut env: VEnv<'db>, + mut comptime_env: CEnv, + body: Vec>, + ) -> FoldOutcome<'db> { + for stmt in body { + match stmt.kind { + MonoStmtKind::Let { id, mode, init, .. } => { + let comptime = mode.is_comptime(); + let init = init.map(|expr| self.eval_expr(&env, &comptime_env, expr)); + let init_is_comptime = init + .as_ref() + .is_some_and(|expr| self.expr_is_comptime(expr, &comptime_env)); + if let Some(expr) = init.filter(|expr| self.expr_is_known_value(expr)) { + env.insert(id.name.clone(), expr); + } else { + env.remove(&id.name); + } + if comptime || init_is_comptime { + comptime_env.insert(id.name); + } else { + comptime_env.remove(&id.name); + } + } + MonoStmtKind::Assign { + op: AssignOp::Plain, + lhs, + rhs, + } => { + let (lhs, target) = self.eval_lvalue(&env, &comptime_env, lhs); + let rhs = self.eval_expr(&env, &comptime_env, rhs); + if let Some(id) = target { + let rhs_is_comptime = self.expr_is_comptime(&rhs, &comptime_env); + if self.expr_is_known_value(&rhs) { + if matches!(&lhs.kind, MonoExprKind::Var(_)) { + env.insert(id.name.clone(), rhs); + if rhs_is_comptime { + comptime_env.insert(id.name); + } else { + comptime_env.remove(&id.name); + } + } else { + env.remove(&id.name); + comptime_env.remove(&id.name); + } + } else { + env.remove(&id.name); + if rhs_is_comptime && matches!(&lhs.kind, MonoExprKind::Var(_)) { + comptime_env.insert(id.name); + } else { + comptime_env.remove(&id.name); + } + } + } + } + MonoStmtKind::Return(expr) => { + let Some(expr) = expr.map(|expr| self.eval_expr(&env, &comptime_env, expr)) + else { + return FoldOutcome::ReturnedUnknownAbort; + }; + return if self.expr_is_known_value(&expr) { + FoldOutcome::ReturnedKnown(expr) + } else { + FoldOutcome::ReturnedUnknownAbort + }; + } + MonoStmtKind::Expr(_) => {} + MonoStmtKind::Match { scrutinees, arms } => { + let scrutinees = scrutinees + .into_iter() + .map(|expr| self.eval_expr(&env, &comptime_env, expr)) + .collect::>(); + let arms = arms + .into_iter() + .map(|arm| self.eval_arm_labels(&env, &comptime_env, arm)) + .collect::>(); + if scrutinees.iter().all(|expr| self.expr_is_known_value(expr)) { + let matched = match_arms_with(&env, &scrutinees, &arms, |expr| { + self.expr_is_known_value(expr) + }); + if let Some((matched_env, body)) = matched { + match self.eval_fun_body( + type_reg, + matched_env, + comptime_env.clone(), + body, + ) { + FoldOutcome::ReturnedKnown(expr) => { + return FoldOutcome::ReturnedKnown(expr); + } + FoldOutcome::ReturnedUnknownAbort => { + return FoldOutcome::ReturnedUnknownAbort; + } + FoldOutcome::FellThroughContinue(next_env, next_comptime_env) => { + env = next_env; + comptime_env = next_comptime_env; + } + } + } + } else { + return FoldOutcome::ReturnedUnknownAbort; + } + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + let cond = self.eval_expr(&env, &comptime_env, cond); + let Some(cond) = known_bool(self.db, &cond) else { + return FoldOutcome::ReturnedUnknownAbort; + }; + let body = if cond { + then_body + } else { + else_body.unwrap_or_default() + }; + match self.eval_fun_body(type_reg, env.clone(), comptime_env.clone(), body) { + FoldOutcome::ReturnedKnown(expr) => { + return FoldOutcome::ReturnedKnown(expr); + } + FoldOutcome::ReturnedUnknownAbort => { + return FoldOutcome::ReturnedUnknownAbort; + } + FoldOutcome::FellThroughContinue(next_env, next_comptime_env) => { + env = next_env; + comptime_env = next_comptime_env; + } + } + } + MonoStmtKind::Block(body) => { + match self.eval_fun_body(type_reg, env.clone(), comptime_env.clone(), body) { + FoldOutcome::ReturnedKnown(expr) => { + return FoldOutcome::ReturnedKnown(expr); + } + FoldOutcome::ReturnedUnknownAbort => { + return FoldOutcome::ReturnedUnknownAbort; + } + FoldOutcome::FellThroughContinue(next_env, next_comptime_env) => { + env = next_env; + comptime_env = next_comptime_env; + } + } + } + MonoStmtKind::Assembly(body) => { + let state = venv_to_yul_state(&env); + let Some(state) = self.eval_yul_block(state, &body) else { + return FoldOutcome::ReturnedUnknownAbort; + }; + env = merge_yul_state(type_reg, state, env); + } + MonoStmtKind::For { .. } + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Assign { + op: + AssignOp::Add + | AssignOp::Sub + | AssignOp::BitXor + | AssignOp::BitAnd + | AssignOp::BitOr + | AssignOp::Mod, + .. + } + | MonoStmtKind::Error => return FoldOutcome::ReturnedUnknownAbort, + } + } + FoldOutcome::FellThroughContinue(env, comptime_env) + } + + fn check_comptime_params( + &mut self, + name: &str, + args: &[MonoExpr<'db>], + comptime_env: &CEnv, + span: Span<'db>, + ) { + if !self.enforce_comptime { + return; + } + let function_name = self + .functions + .get(name) + .map(|function| display_mono_function_name(self.db, function)) + .unwrap_or_else(|| display_backend_symbol(name)); + let contexts = self + .functions + .get(name) + .map(|function| { + function + .params + .iter() + .zip(args) + .filter(|(param, arg)| { + param_is_comptime(self.db, param) + && !self.expr_is_comptime(arg, comptime_env) + }) + .map(|(param, _)| param.name.clone()) + .collect::>() + }) + .unwrap_or_default(); + for param in contexts { + self.comptime_failed( + format!( + "runtime value passed to comptime parameter '{}' of '{}'", + param, function_name + ), + Some(span), + ); + } + } + + fn expr_is_comptime(&self, expr: &MonoExpr<'db>, comptime_env: &CEnv) -> bool { + if self.expr_is_known_value(expr) { + return true; + } + match &expr.kind { + MonoExprKind::Var(id) => comptime_env.contains(&id.name), + MonoExprKind::Lit(_) | MonoExprKind::Proxy(_) => true, + MonoExprKind::Tuple(elems) => elems + .iter() + .all(|expr| self.expr_is_comptime(expr, comptime_env)), + MonoExprKind::Call { + callee, + args, + origin, + } => { + let callee_is_comptime = match origin { + MonoCallOrigin::Builtin(intrinsic) => intrinsic_is_pure(*intrinsic), + MonoCallOrigin::Source(_) | MonoCallOrigin::ByName => { + self.pure_funs.contains(&callee.name) + } + }; + callee_is_comptime + && args + .iter() + .all(|arg| self.expr_is_comptime(arg, comptime_env)) + } + MonoExprKind::Con { args, .. } => args + .iter() + .all(|arg| self.expr_is_comptime(arg, comptime_env)), + MonoExprKind::ClosureDispatch { .. } => false, + MonoExprKind::BinOp { lhs, rhs, .. } => { + self.expr_is_comptime(lhs, comptime_env) && self.expr_is_comptime(rhs, comptime_env) + } + MonoExprKind::UnaryOp { expr, .. } => self.expr_is_comptime(expr, comptime_env), + MonoExprKind::Index { base, index } => { + self.expr_is_comptime(base, comptime_env) + && self.expr_is_comptime(index, comptime_env) + } + MonoExprKind::StorageIndex { .. } => false, + MonoExprKind::Field { base, .. } => self.expr_is_comptime(base, comptime_env), + MonoExprKind::Conversion { expr, .. } => self.expr_is_comptime(expr, comptime_env), + MonoExprKind::Match { scrutinee, arms } => { + self.expr_is_comptime(scrutinee, comptime_env) + && arms + .iter() + .all(|arm| self.expr_is_comptime(&arm.expr, comptime_env)) + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => { + self.expr_is_comptime(cond, comptime_env) + && self.expr_is_comptime(then_expr, comptime_env) + && self.expr_is_comptime(else_expr, comptime_env) + } + MonoExprKind::Lambda { .. } => true, + MonoExprKind::Error => false, + } + } + + fn eval_yul_block(&mut self, mut state: YulState, body: &[YulStmt<'db>]) -> Option { + for stmt in body { + state = self.eval_yul_stmt(state, stmt)?; + } + Some(state) + } + + fn eval_yul_stmt(&mut self, mut state: YulState, stmt: &YulStmt<'db>) -> Option { + match &stmt.kind { + YulStmtKind::Assign { names, value } if names.len() == 1 => { + let value = self.eval_yul_expr(&state, value)?; + state.insert(ident_text(self.db, &names[0]), value); + Some(state) + } + YulStmtKind::Expr(YulExpr { + kind: YulExprKind::Call { name, args }, + .. + }) if ident_text(self.db, name) == "mstore" && args.len() == 2 => { + if !self.comptime_mode { + return None; + } + let offset = self.eval_yul_expr(&state, &args[0])?; + let value = self.eval_yul_expr(&state, &args[1])?; + self.mstore(offset, value); + Some(state) + } + YulStmtKind::Expr(YulExpr { + kind: YulExprKind::Call { name, args }, + .. + }) if ident_text(self.db, name) == "mstore8" && args.len() == 2 => { + if !self.comptime_mode { + return None; + } + let offset = self.eval_yul_expr(&state, &args[0])?; + let value = self.eval_yul_expr(&state, &args[1])?; + self.memory.insert(offset, word_low_byte(&value)); + Some(state) + } + _ => None, + } + } + + fn eval_yul_expr(&mut self, state: &YulState, expr: &YulExpr<'db>) -> Option { + match &expr.kind { + YulExprKind::Ident(name) => state.get(&ident_text(self.db, name)).cloned(), + YulExprKind::Lit(YulLitKind::Number(text)) => BigInt::from_decimal_str(text), + YulExprKind::Lit(YulLitKind::Hex(text)) => BigInt::from_hex_str(text), + YulExprKind::Lit(YulLitKind::Bool(value)) => Some(BigInt::from_u64(u64::from(*value))), + YulExprKind::Call { name, args } + if ident_text(self.db, name) == "mload" && args.len() == 1 => + { + if !self.comptime_mode { + return None; + } + let offset = self.eval_yul_expr(state, &args[0])?; + self.mload(offset) + } + YulExprKind::Call { name, args } => { + let values = args + .iter() + .map(|arg| self.eval_yul_expr(state, arg)) + .collect::>>()?; + eval_yul_op(&ident_text(self.db, name), &values) + } + YulExprKind::Lit(YulLitKind::String(_)) + | YulExprKind::Lit(YulLitKind::Error) + | YulExprKind::Error => None, + } + } + + fn mstore(&mut self, offset: BigInt, value: BigInt) { + let bytes = value.mod_word().to_word_be_bytes(); + for (index, byte) in bytes.into_iter().enumerate() { + self.memory + .insert(offset.add(&BigInt::from_u64(index as u64)), byte); + } + } + + fn mload(&self, offset: BigInt) -> Option { + let mut bytes = [0u8; 32]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = *self + .memory + .get(&offset.add(&BigInt::from_u64(index as u64)))?; + } + Some(BigInt::from_be_bytes(&bytes)) + } + + fn with_comptime_mode(&mut self, f: impl FnOnce(&mut Self) -> T) -> T { + let old = self.comptime_mode; + self.comptime_mode = true; + let result = f(self); + self.comptime_mode = old; + result + } + + fn comptime_failed(&mut self, context: impl Into, span: Option>) { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::ComptimeEvaluationFailed { + context: context.into(), + }, + span, + }); + } +} + +#[derive(Debug, Clone, Copy)] +enum WordBinaryOp { + Add, + Sub, + Mul, + Gt, + BitXor, + BitAnd, + BitOr, + Eq, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inline_depth_limit_is_independent_of_total_work_fuel() { + let exhaustion = classify_inline_budget_exhaustion(4_096, 4_096, 128, 128); + + assert_eq!( + exhaustion, + Some(InlineBudgetExhaustion::InlineDepth { limit: 128 }) + ); + assert_eq!(exhaustion.unwrap().diagnostic_limit(), 128); + assert_eq!( + classify_inline_budget_exhaustion(4_096, 4_096, 127, 128), + None + ); + } +} diff --git a/crates/specialize/src/evaluate/dead_code.rs b/crates/specialize/src/evaluate/dead_code.rs new file mode 100644 index 00000000..8f8ebe12 --- /dev/null +++ b/crates/specialize/src/evaluate/dead_code.rs @@ -0,0 +1,74 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::ir::{ + MonoCallOrigin, MonoExpr, MonoExprKind, MonoItem, MonoModule, MonoPat, MonoStmt, + visit::{Visitor, walk_expr}, +}; + +pub(super) fn eliminate_dead_functions<'db>(mut module: MonoModule<'db>) -> MonoModule<'db> { + let functions = module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => Some((function.name.clone(), function)), + _ => None, + }) + .collect::>(); + let function_names = functions.keys().cloned().collect::>(); + let mut used = BTreeSet::new(); + let mut work = module.entry_points.clone(); + while let Some(name) = work.pop() { + if !used.insert(name.clone()) { + continue; + } + if let Some(function) = functions.get(&name) { + for call in calls_in_stmts(&function.body, &function_names) { + if functions.contains_key(&call) && !used.contains(&call) { + work.push(call); + } + } + } + } + module.items.retain(|item| match item { + MonoItem::Function(function) => used.contains(&function.name), + _ => true, + }); + module +} + +fn calls_in_stmts(stmts: &[MonoStmt<'_>], function_names: &BTreeSet) -> BTreeSet { + let mut collector = CallCollector { + calls: BTreeSet::new(), + function_names, + }; + for stmt in stmts { + collector.visit_stmt(stmt); + } + collector.calls +} + +struct CallCollector<'functions> { + calls: BTreeSet, + function_names: &'functions BTreeSet, +} + +impl<'db> Visitor<'db> for CallCollector<'_> { + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + match &expr.kind { + MonoExprKind::Call { callee, origin, .. } => { + if !matches!(origin, MonoCallOrigin::Builtin(_)) { + self.calls.insert(callee.name.clone()); + } + walk_expr(self, expr); + } + MonoExprKind::Var(id) if self.function_names.contains(&id.name) => { + self.calls.insert(id.name.clone()); + } + _ => walk_expr(self, expr), + } + } + + fn visit_pat(&mut self, _pat: &MonoPat<'db>) { + // Existing dead-code call collection ignored match pattern labels. + } +} diff --git a/crates/specialize/src/evaluate/effects.rs b/crates/specialize/src/evaluate/effects.rs new file mode 100644 index 00000000..0e606112 --- /dev/null +++ b/crates/specialize/src/evaluate/effects.rs @@ -0,0 +1,554 @@ +use hir::{ + Db as HirDb, + anchor::DefId, + ast::item::{ContractDef, Item, Module}, +}; +use hir_ty::Db; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{ + assigned::AssignedNames, + ident_text, + known::{collect_pat_binders, lvalue_root_name}, + yul_const::asm_is_interpretable, +}; +use crate::ir::{ + MonoCallOrigin, MonoExpr, MonoExprKind, MonoFunction, MonoIntrinsic, MonoItem, MonoModule, + MonoStmt, MonoStmtKind, + visit::{Visitor, walk_expr}, +}; + +pub(super) fn compute_pure_funs<'db>( + db: &'db dyn Db, + functions: &FxHashMap>, + storage_fields: &FxHashSet, +) -> FxHashSet { + let mut pure = FxHashSet::default(); + loop { + let before = pure.len(); + for (name, function) in functions { + if pure.contains(name) || name == "revertLit" { + continue; + } + if function_is_pure(db, function, &pure, name, storage_fields) { + pure.insert(name.clone()); + } + } + if pure.len() == before { + return pure; + } + } +} + +pub(super) fn intrinsic_is_pure(intrinsic: MonoIntrinsic) -> bool { + matches!( + intrinsic, + MonoIntrinsic::PrimAddWord + | MonoIntrinsic::PrimEqWord + | MonoIntrinsic::SubWord + | MonoIntrinsic::MulWord + | MonoIntrinsic::GtWord + | MonoIntrinsic::BxorWord + | MonoIntrinsic::BandWord + | MonoIntrinsic::BorWord + | MonoIntrinsic::WordToInteger + | MonoIntrinsic::WordFromInteger + | MonoIntrinsic::IntegerAdd + | MonoIntrinsic::IntegerSub + | MonoIntrinsic::IntegerMul + | MonoIntrinsic::IntegerLt + | MonoIntrinsic::IntegerEq + | MonoIntrinsic::ConcatLit + | MonoIntrinsic::StrlenLit + | MonoIntrinsic::KeccakLit + ) +} + +fn function_is_pure<'db>( + db: &'db dyn Db, + function: &MonoFunction<'db>, + pure: &FxHashSet, + self_name: &str, + storage_fields: &FxHashSet, +) -> bool { + let mut locals = function + .params + .iter() + .map(|param| param.name.clone()) + .collect::>(); + stmts_are_pure( + db, + &function.body, + pure, + self_name, + storage_fields, + &mut locals, + ) +} + +fn stmts_are_pure<'db>( + db: &'db dyn Db, + stmts: &[MonoStmt<'db>], + pure: &FxHashSet, + self_name: &str, + storage_fields: &FxHashSet, + locals: &mut FxHashSet, +) -> bool { + for stmt in stmts { + if !stmt_is_pure(db, stmt, pure, self_name, storage_fields, locals) { + return false; + } + } + true +} + +fn stmt_is_pure<'db>( + db: &'db dyn Db, + stmt: &MonoStmt<'db>, + pure: &FxHashSet, + self_name: &str, + storage_fields: &FxHashSet, + locals: &mut FxHashSet, +) -> bool { + match &stmt.kind { + MonoStmtKind::Let { id, init, .. } => { + if !init + .as_ref() + .is_none_or(|expr| expr_is_pure(expr, pure, self_name)) + { + return false; + } + locals.insert(id.name.clone()); + true + } + MonoStmtKind::Return(expr) => expr + .as_ref() + .is_none_or(|expr| expr_is_pure(expr, pure, self_name)), + MonoStmtKind::Expr(expr) => expr_is_pure(expr, pure, self_name), + MonoStmtKind::Assign { lhs, rhs, .. } => { + !lvalue_writes_storage(lhs, storage_fields, locals) + && expr_is_pure(lhs, pure, self_name) + && expr_is_pure(rhs, pure, self_name) + } + MonoStmtKind::Match { scrutinees, arms } => { + scrutinees + .iter() + .all(|expr| expr_is_pure(expr, pure, self_name)) + && arms.iter().all(|arm| { + let mut arm_locals = locals.clone(); + for pat in &arm.pats { + collect_pat_binders(pat, &mut arm_locals); + } + stmts_are_pure( + db, + &arm.body, + pure, + self_name, + storage_fields, + &mut arm_locals, + ) + }) + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + let mut loop_locals = locals.clone(); + let mut post_locals = loop_locals.clone(); + stmts_are_pure(db, init, pure, self_name, storage_fields, &mut loop_locals) + && expr_is_pure(cond, pure, self_name) + && stmts_are_pure(db, post, pure, self_name, storage_fields, &mut post_locals) + && stmts_are_pure(db, body, pure, self_name, storage_fields, &mut loop_locals) + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + let mut then_locals = locals.clone(); + let mut else_locals = locals.clone(); + expr_is_pure(cond, pure, self_name) + && stmts_are_pure( + db, + then_body, + pure, + self_name, + storage_fields, + &mut then_locals, + ) + && else_body.as_ref().is_none_or(|body| { + stmts_are_pure(db, body, pure, self_name, storage_fields, &mut else_locals) + }) + } + MonoStmtKind::Block(body) => { + let mut block_locals = locals.clone(); + stmts_are_pure(db, body, pure, self_name, storage_fields, &mut block_locals) + } + MonoStmtKind::Assembly(body) => asm_is_interpretable(db, body), + MonoStmtKind::Break | MonoStmtKind::Continue => true, + MonoStmtKind::Error => false, + } +} + +fn expr_is_pure(expr: &MonoExpr<'_>, pure: &FxHashSet, self_name: &str) -> bool { + let mut visitor = ExprPurityVisitor { + pure, + self_name, + is_pure: true, + }; + visitor.visit_expr(expr); + visitor.is_pure +} + +struct ExprPurityVisitor<'pure> { + pure: &'pure FxHashSet, + self_name: &'pure str, + is_pure: bool, +} + +impl<'pure, 'db> Visitor<'db> for ExprPurityVisitor<'pure> { + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + if !self.is_pure { + return; + } + match &expr.kind { + MonoExprKind::Call { + callee, + args, + origin, + } => { + let callee_is_pure = match origin { + MonoCallOrigin::Builtin(intrinsic) => intrinsic_is_pure(*intrinsic), + MonoCallOrigin::Source(_) | MonoCallOrigin::ByName => { + source_callee_is_pure(&callee.name, self.pure, self.self_name) + } + }; + if !callee_is_pure { + self.is_pure = false; + return; + } + for arg in args { + self.visit_expr(arg); + } + } + MonoExprKind::ClosureDispatch { .. } + | MonoExprKind::StorageIndex { .. } + | MonoExprKind::Error => { + self.is_pure = false; + } + MonoExprKind::Lambda { .. } + | MonoExprKind::Lit(_) + | MonoExprKind::Var(_) + | MonoExprKind::Proxy(_) => {} + _ => walk_expr(self, expr), + } + } +} + +fn source_callee_is_pure(callee: &str, pure: &FxHashSet, self_name: &str) -> bool { + callee == self_name || pure.contains(callee) +} + +pub(super) fn compute_write_effects<'db>( + functions: &FxHashMap>, + storage_fields: &FxHashSet, +) -> FxHashMap { + let mut effects = functions + .keys() + .map(|name| (name.clone(), AssignedNames::empty())) + .collect::>(); + loop { + let mut changed = false; + for (name, function) in functions { + let next = function_write_effects(function, storage_fields, &effects); + if effects.get(name) != Some(&next) { + effects.insert(name.clone(), next); + changed = true; + } + } + if !changed { + return effects; + } + } +} + +fn function_write_effects<'db>( + function: &MonoFunction<'db>, + storage_fields: &FxHashSet, + call_effects: &FxHashMap, +) -> AssignedNames { + let mut locals = function + .params + .iter() + .map(|param| param.name.clone()) + .collect::>(); + let mut effects = AssignedNames::empty(); + collect_write_effects_in_stmts( + &function.body, + storage_fields, + call_effects, + &mut locals, + &mut effects, + ); + effects +} + +fn collect_write_effects_in_stmts<'db>( + stmts: &[MonoStmt<'db>], + storage_fields: &FxHashSet, + call_effects: &FxHashMap, + locals: &mut FxHashSet, + effects: &mut AssignedNames, +) { + for stmt in stmts { + match &stmt.kind { + MonoStmtKind::Let { id, init, .. } => { + if let Some(init) = init { + effects.merge(expr_write_effects_from_call_summaries(init, call_effects)); + } + locals.insert(id.name.clone()); + } + MonoStmtKind::Return(expr) => { + if let Some(expr) = expr { + effects.merge(expr_write_effects_from_call_summaries(expr, call_effects)); + } + } + MonoStmtKind::Expr(expr) => { + effects.merge(expr_write_effects_from_call_summaries(expr, call_effects)); + } + MonoStmtKind::Assign { lhs, rhs, .. } => { + if lvalue_writes_storage(lhs, storage_fields, locals) { + if let Some(name) = lvalue_root_name(lhs) { + effects.insert(name); + } else { + effects.merge(AssignedNames::All); + } + } + effects.merge(expr_write_effects_from_call_summaries(lhs, call_effects)); + effects.merge(expr_write_effects_from_call_summaries(rhs, call_effects)); + } + MonoStmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + effects.merge(expr_write_effects_from_call_summaries( + scrutinee, + call_effects, + )); + } + for arm in arms { + let mut arm_locals = locals.clone(); + for pat in &arm.pats { + collect_pat_binders(pat, &mut arm_locals); + } + collect_write_effects_in_stmts( + &arm.body, + storage_fields, + call_effects, + &mut arm_locals, + effects, + ); + } + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + let mut loop_locals = locals.clone(); + collect_write_effects_in_stmts( + init, + storage_fields, + call_effects, + &mut loop_locals, + effects, + ); + effects.merge(expr_write_effects_from_call_summaries(cond, call_effects)); + let mut post_locals = loop_locals.clone(); + collect_write_effects_in_stmts( + post, + storage_fields, + call_effects, + &mut post_locals, + effects, + ); + collect_write_effects_in_stmts( + body, + storage_fields, + call_effects, + &mut loop_locals, + effects, + ); + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + effects.merge(expr_write_effects_from_call_summaries(cond, call_effects)); + let mut then_locals = locals.clone(); + collect_write_effects_in_stmts( + then_body, + storage_fields, + call_effects, + &mut then_locals, + effects, + ); + if let Some(else_body) = else_body { + let mut else_locals = locals.clone(); + collect_write_effects_in_stmts( + else_body, + storage_fields, + call_effects, + &mut else_locals, + effects, + ); + } + } + MonoStmtKind::Block(body) => { + let mut block_locals = locals.clone(); + collect_write_effects_in_stmts( + body, + storage_fields, + call_effects, + &mut block_locals, + effects, + ); + } + MonoStmtKind::Assembly(_) => effects.merge(AssignedNames::All), + MonoStmtKind::Break | MonoStmtKind::Continue | MonoStmtKind::Error => {} + } + } +} + +pub(super) fn expr_write_effects_from_call_summaries<'db>( + expr: &MonoExpr<'db>, + call_effects: &FxHashMap, +) -> AssignedNames { + let mut visitor = SummaryWriteEffectsVisitor { + call_effects, + effects: AssignedNames::empty(), + }; + visitor.visit_expr(expr); + visitor.effects +} + +struct SummaryWriteEffectsVisitor<'effects> { + call_effects: &'effects FxHashMap, + effects: AssignedNames, +} + +impl<'effects, 'db> Visitor<'db> for SummaryWriteEffectsVisitor<'effects> { + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + match &expr.kind { + MonoExprKind::Call { + callee, + args, + origin, + } => { + for arg in args { + self.visit_expr(arg); + } + if !matches!(origin, MonoCallOrigin::Builtin(_)) { + self.effects.merge( + self.call_effects + .get(&callee.name) + .cloned() + .unwrap_or(AssignedNames::All), + ); + } + } + MonoExprKind::ClosureDispatch { callee, args } => { + self.visit_expr(callee); + for arg in args { + self.visit_expr(arg); + } + self.effects.merge(AssignedNames::All); + } + MonoExprKind::Lambda { .. } => {} + _ => walk_expr(self, expr), + } + } +} + +fn lvalue_writes_storage( + lhs: &MonoExpr<'_>, + storage_fields: &FxHashSet, + locals: &FxHashSet, +) -> bool { + expr_contains_storage_index(lhs) + || lvalue_root_name(lhs) + .is_some_and(|name| storage_fields.contains(&name) && !locals.contains(&name)) +} + +fn expr_contains_storage_index(expr: &MonoExpr<'_>) -> bool { + let mut visitor = StorageIndexFinder { found: false }; + visitor.visit_expr(expr); + visitor.found +} + +struct StorageIndexFinder { + found: bool, +} + +impl<'db> Visitor<'db> for StorageIndexFinder { + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + if self.found { + return; + } + match &expr.kind { + MonoExprKind::StorageIndex { .. } => { + self.found = true; + } + MonoExprKind::Lambda { .. } => {} + _ => walk_expr(self, expr), + } + } +} + +pub(super) fn storage_field_names<'db>( + db: &'db dyn Db, + module: &MonoModule<'db>, +) -> FxHashSet { + let mut fields = FxHashSet::default(); + for item in &module.items { + let MonoItem::Contract(contract) = item else { + continue; + }; + let parsed = parse_file_to_hir(db, contract.def.file(db)).module(db); + if let Some(contract_def) = find_contract(db, parsed, contract.def) { + for field in contract_def.fields(db) { + fields.insert(ident_text(db, field.name())); + } + } + } + fields +} + +fn find_contract<'db>( + db: &'db dyn HirDb, + module: Module<'db>, + def: DefId<'db>, +) -> Option> { + module.items(db).iter().find_map(|item| match item { + Item::ContractDef(contract) if contract.def_id_value(db) == def => Some(*contract), + _ => None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn self_recursion_does_not_require_cloning_the_known_pure_set() { + let pure = FxHashSet::from_iter(["known".to_owned()]); + + assert!(source_callee_is_pure("candidate", &pure, "candidate")); + assert!(source_callee_is_pure("known", &pure, "candidate")); + assert!(!source_callee_is_pure("impure", &pure, "candidate")); + assert_eq!(pure, FxHashSet::from_iter(["known".to_owned()])); + } +} diff --git a/crates/specialize/src/evaluate/erasure.rs b/crates/specialize/src/evaluate/erasure.rs new file mode 100644 index 00000000..aaa49a1d --- /dev/null +++ b/crates/specialize/src/evaluate/erasure.rs @@ -0,0 +1,267 @@ +use hir::span::Span; +use hir_ty::{BuiltinTyCtor, ConversionKind, Db, Ty, TyCtor, TyKind}; + +use super::core::Evaluator; +use crate::{ + ir::{ + MonoCallOrigin, MonoExpr, MonoExprKind, MonoFunction, MonoItem, MonoModule, MonoParam, + MonoPat, MonoPatKind, MonoStmt, MonoStmtKind, + visit::{Visitor, walk_expr, walk_pat, walk_stmt}, + }, + specialize::{SpecializeDiagnostic, SpecializeDiagnosticKind, display_backend_ty}, +}; + +pub(super) fn param_is_comptime<'db>(db: &'db dyn Db, param: &MonoParam<'db>) -> bool { + param.mode.is_comptime() || ty_is_comptime(db, param.ty.ty()) +} + +pub(super) fn ty_is_comptime<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + matches!(ty.kind(db), TyKind::Comptime(_)) +} + +pub(super) fn display_mono_function_name<'db>( + db: &'db dyn Db, + function: &MonoFunction<'db>, +) -> String { + function + .source + .and_then(|def| def.name(db)) + .unwrap_or_else(|| display_backend_symbol(&function.name)) +} + +fn display_call_name<'db>(db: &'db dyn Db, origin: MonoCallOrigin<'db>, fallback: &str) -> String { + match origin { + MonoCallOrigin::Source(def) => def + .name(db) + .unwrap_or_else(|| display_backend_symbol(fallback)), + MonoCallOrigin::Builtin(_) | MonoCallOrigin::ByName => display_backend_symbol(fallback), + } +} + +pub(super) fn display_backend_symbol(name: &str) -> String { + let base = name.split_once('$').map_or(name, |(base, _)| base); + let base = strip_hash_suffix(base).unwrap_or(base); + let base = base.strip_prefix("main_").unwrap_or(base); + if let Some((owner, member)) = base.split_once('_') + && owner.chars().next().is_some_and(char::is_uppercase) + { + return format!("{owner}.{member}"); + } + base.to_owned() +} + +fn strip_hash_suffix(name: &str) -> Option<&str> { + let (base, suffix) = name.rsplit_once('_')?; + let hex = suffix.strip_prefix('d')?; + (hex.len() == 8 && hex.chars().all(|ch| ch.is_ascii_hexdigit())).then_some(base) +} + +pub(super) fn ty_is_function<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + matches!(ty.kind(db), TyKind::Function { .. }) +} + +pub(super) fn lambda_ret_is_comptime<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + matches!( + ty.kind(db), + TyKind::Function { ret, .. } if ty_is_comptime(db, *ret) + ) +} + +pub(super) fn ty_is_builtin<'db>(db: &'db dyn Db, ty: Ty<'db>, builtin: BuiltinTyCtor) -> bool { + let ty = strip_comptime(db, ty); + matches!( + ty.kind(db), + TyKind::Named { + ctor: TyCtor::Builtin(ctor), + args, + } if *ctor == builtin && args.is_empty() + ) +} + +fn ty_needs_erasure<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Comptime(_) => true, + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Integer), + args, + } if args.is_empty() => true, + TyKind::Named { args, .. } => args.iter().any(|arg| ty_needs_erasure(db, *arg)), + TyKind::Function { params, ret } => { + params.iter().any(|param| ty_needs_erasure(db, *param)) || ty_needs_erasure(db, *ret) + } + TyKind::Tuple(elems) => elems.iter().any(|elem| ty_needs_erasure(db, *elem)), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + } +} + +fn strip_comptime<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(db) { + TyKind::Comptime(inner) => strip_comptime(db, *inner), + _ => ty, + } +} + +impl<'db> Evaluator<'db> { + pub(super) fn check_integer_erasure(&mut self, module: &MonoModule<'db>) { + for item in &module.items { + let MonoItem::Function(function) = item else { + continue; + }; + if self.check_erasure_ty( + format!( + "return type of `{}`", + display_mono_function_name(self.db, function) + ), + function.ret.ty(), + Some(function.span), + ) { + continue; + } + for param in &function.params { + self.check_erasure_ty( + format!("parameter '{}'", param.name), + param.ty.ty(), + Some(param.span), + ); + } + self.check_integer_erasure_stmts(&function.body); + } + } + + fn check_integer_erasure_stmts(&mut self, stmts: &[MonoStmt<'db>]) { + for stmt in stmts { + self.visit_stmt(stmt); + } + } + + fn check_erasure_ty( + &mut self, + context: impl Into, + ty: Ty<'db>, + span: Option>, + ) -> bool { + let needs_erasure = ty_needs_erasure(self.db, ty); + if needs_erasure { + self.integer_erasure(context.into(), ty, span); + } + needs_erasure + } + + fn integer_erasure(&mut self, context: String, ty: Ty<'db>, span: Option>) { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::IntegerErasure { + context, + ty: display_backend_ty(self.db, ty), + }, + span, + }); + } +} + +impl<'db> Visitor<'db> for Evaluator<'db> { + fn visit_stmt(&mut self, stmt: &MonoStmt<'db>) { + if let MonoStmtKind::Let { id, ty, init, .. } = &stmt.kind { + let mut failed = + self.check_erasure_ty(format!("let '{}'", id.name), id.ty.ty(), Some(stmt.span)); + if let Some(ty) = ty { + failed |= self.check_erasure_ty( + format!("let annotation '{}'", id.name), + ty.ty(), + Some(stmt.span), + ); + } + if failed { + return; + } + if let Some(init) = init { + self.visit_expr(init); + } + return; + } + walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + if self.check_erasure_ty("expression", expr.ty.ty(), Some(expr.span)) { + return; + } + match &expr.kind { + MonoExprKind::Var(id) => { + self.check_erasure_ty( + format!("variable '{}'", id.name), + id.ty.ty(), + Some(expr.span), + ); + } + MonoExprKind::Call { callee, origin, .. } => { + if self.check_erasure_ty( + format!( + "call to `{}`", + display_call_name(self.db, *origin, &callee.name) + ), + callee.ty.ty(), + Some(expr.span), + ) { + return; + } + walk_expr(self, expr); + } + MonoExprKind::Con { ctor, .. } => { + if self.check_erasure_ty( + format!("constructor `{}`", display_backend_symbol(&ctor.name)), + ctor.ty.ty(), + Some(expr.span), + ) { + return; + } + walk_expr(self, expr); + } + MonoExprKind::Proxy(ty) => { + self.check_erasure_ty("proxy", ty.ty(), Some(expr.span)); + } + MonoExprKind::Conversion { + expr: inner, + ty, + kind: + ConversionKind::Identity + | ConversionKind::ValueTypeWrap + | ConversionKind::ValueTypeUnwrap, + } => { + self.visit_expr(inner); + self.check_erasure_ty("explicit conversion", ty.ty(), Some(expr.span)); + } + MonoExprKind::Lit(_) | MonoExprKind::Lambda { .. } | MonoExprKind::Error => {} + _ => walk_expr(self, expr), + } + } + + fn visit_pat(&mut self, pat: &MonoPat<'db>) { + if self.check_erasure_ty("pattern", pat.ty.ty(), Some(pat.span)) { + return; + } + match &pat.kind { + MonoPatKind::Var(id) => { + self.check_erasure_ty( + format!("pattern variable '{}'", id.name), + id.ty.ty(), + Some(pat.span), + ); + } + MonoPatKind::Con { ctor, .. } => { + if self.check_erasure_ty( + format!( + "pattern constructor `{}`", + display_backend_symbol(&ctor.name) + ), + ctor.ty.ty(), + Some(pat.span), + ) { + return; + } + walk_pat(self, pat); + } + MonoPatKind::Tuple(_) | MonoPatKind::ComptimeLabel(_) => walk_pat(self, pat), + MonoPatKind::Wildcard | MonoPatKind::Lit(_) | MonoPatKind::Error => {} + } + } +} diff --git a/crates/specialize/src/evaluate/known.rs b/crates/specialize/src/evaluate/known.rs new file mode 100644 index 00000000..44c991ad --- /dev/null +++ b/crates/specialize/src/evaluate/known.rs @@ -0,0 +1,334 @@ +use hir::{ast::function::LitKind, span::Span}; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{CEnv, TypeReg, VEnv, assigned::AssignedNames, value::BigInt}; +use crate::ir::{ + MonoArm, MonoBuiltinCtor, MonoExpr, MonoExprKind, MonoId, MonoParam, MonoPat, MonoPatKind, + MonoStmt, MonoStmtKind, MonoTy, + visit::{Visitor, walk_pat, walk_stmt}, +}; + +pub(super) fn build_type_reg<'db>( + params: &[MonoParam<'db>], + body: &[MonoStmt<'db>], +) -> TypeReg<'db> { + let mut reg = FxHashMap::default(); + for param in params { + reg.insert( + param.name.clone(), + MonoId { + name: param.name.clone(), + ty: param.ty, + span: param.span, + }, + ); + } + collect_type_reg_stmts(body, &mut reg); + reg +} + +fn collect_type_reg_stmts<'db>(stmts: &[MonoStmt<'db>], reg: &mut TypeReg<'db>) { + let mut collector = TypeRegCollector { reg }; + for stmt in stmts { + collector.visit_stmt(stmt); + } +} + +struct TypeRegCollector<'reg, 'db> { + reg: &'reg mut TypeReg<'db>, +} + +impl<'reg, 'db> Visitor<'db> for TypeRegCollector<'reg, 'db> { + fn visit_stmt(&mut self, stmt: &MonoStmt<'db>) { + if let MonoStmtKind::Let { id, .. } = &stmt.kind { + self.reg.insert(id.name.clone(), id.clone()); + } + walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, _expr: &MonoExpr<'db>) {} + + fn visit_pat(&mut self, _pat: &MonoPat<'db>) {} +} + +pub(super) fn known_int(expr: &MonoExpr<'_>) -> Option { + match &expr.kind { + MonoExprKind::Lit(LitKind::Number(text)) => BigInt::from_decimal_str(text), + MonoExprKind::Lit(LitKind::Hex(text)) => BigInt::from_hex_str(text), + MonoExprKind::Conversion { expr, .. } => known_int(expr), + _ => None, + } +} + +pub(super) fn known_string(expr: &MonoExpr<'_>) -> Option { + match &expr.kind { + MonoExprKind::Lit(LitKind::String(text)) => decode_string_lit(text), + MonoExprKind::Conversion { expr, .. } => known_string(expr), + _ => None, + } +} + +pub(super) fn known_bool<'db>(db: &'db dyn hir_ty::Db, expr: &MonoExpr<'db>) -> Option { + match &expr.kind { + MonoExprKind::Con { ctor, .. } => match ctor.builtin_ctor(db) { + Some(MonoBuiltinCtor::True | MonoBuiltinCtor::Inr) => Some(true), + Some(MonoBuiltinCtor::False | MonoBuiltinCtor::Inl) => Some(false), + _ => None, + }, + MonoExprKind::Conversion { expr, .. } => known_bool(db, expr), + _ => None, + } +} + +pub(super) fn literal_from_known_expr(expr: &MonoExpr<'_>) -> Option { + match &expr.kind { + MonoExprKind::Lit(lit) => Some(lit.clone()), + MonoExprKind::Conversion { expr, .. } => literal_from_known_expr(expr), + _ => None, + } +} + +pub(super) fn int_expr<'db>(value: BigInt, ty: MonoTy<'db>, span: Span<'db>) -> MonoExpr<'db> { + MonoExpr { + span, + ty, + kind: MonoExprKind::Lit(LitKind::Number(value.to_decimal_string())), + } +} + +pub(super) fn string_expr<'db>(value: String, ty: MonoTy<'db>, span: Span<'db>) -> MonoExpr<'db> { + MonoExpr { + span, + ty, + kind: MonoExprKind::Lit(LitKind::String(encode_string_lit(&value))), + } +} + +pub(super) fn bool_expr<'db>(value: bool, ty: MonoTy<'db>, span: Span<'db>) -> MonoExpr<'db> { + let name = if value { + MonoBuiltinCtor::True.name() + } else { + MonoBuiltinCtor::False.name() + } + .to_owned(); + MonoExpr { + span, + ty, + kind: MonoExprKind::Con { + ctor: MonoId { name, ty, span }, + args: Vec::new(), + }, + } +} + +pub(super) fn match_arms_with<'db, F>( + env: &VEnv<'db>, + scrutinees: &[MonoExpr<'db>], + arms: &[MonoArm<'db>], + is_known: F, +) -> Option<(VEnv<'db>, Vec>)> +where + F: Fn(&MonoExpr<'db>) -> bool + Copy, +{ + arms.iter().find_map(|arm| { + if arm.pats.len() != scrutinees.len() { + return None; + } + let mut env = env.clone(); + for (pat, value) in arm.pats.iter().zip(scrutinees) { + env = match_pat(env, pat, value, is_known)?; + } + Some((env, arm.body.clone())) + }) +} + +pub(super) fn match_expr_arms_with<'db, F>( + env: &VEnv<'db>, + scrutinee: &MonoExpr<'db>, + arms: &[crate::ir::MonoExprArm<'db>], + is_known: F, +) -> Option<(VEnv<'db>, MonoExpr<'db>)> +where + F: Fn(&MonoExpr<'db>) -> bool + Copy, +{ + arms.iter().find_map(|arm| { + let env = match_pat(env.clone(), &arm.pat, scrutinee, is_known)?; + Some((env, arm.expr.clone())) + }) +} + +fn match_pat<'db, F>( + mut env: VEnv<'db>, + pat: &MonoPat<'db>, + value: &MonoExpr<'db>, + is_known: F, +) -> Option> +where + F: Fn(&MonoExpr<'db>) -> bool + Copy, +{ + match &pat.kind { + MonoPatKind::Wildcard => Some(env), + MonoPatKind::Var(id) => { + if is_known(value) { + env.insert(id.name.clone(), value.clone()); + } else { + env.remove(&id.name); + } + Some(env) + } + MonoPatKind::Lit(lit) => literal_matches(lit, value).then_some(env), + MonoPatKind::Con { ctor, args } => match &value.kind { + MonoExprKind::Con { + ctor: value_ctor, + args: value_args, + } if constructor_matches(pat.ty, &ctor.name, value.ty, &value_ctor.name) + && args.len() == value_args.len() => + { + for (pat, value) in args.iter().zip(value_args) { + env = match_pat(env, pat, value, is_known)?; + } + Some(env) + } + _ => None, + }, + MonoPatKind::Tuple(pats) => match &value.kind { + MonoExprKind::Tuple(values) if pats.len() == values.len() => { + for (pat, value) in pats.iter().zip(values) { + env = match_pat(env, pat, value, is_known)?; + } + Some(env) + } + _ => None, + }, + MonoPatKind::ComptimeLabel(expr) => literal_from_known_expr(expr) + .is_some_and(|lit| literal_matches(&lit, value)) + .then_some(env), + MonoPatKind::Error => None, + } +} + +fn constructor_matches( + _pat_ty: MonoTy<'_>, + pat_ctor: &str, + _value_ty: MonoTy<'_>, + value_ctor: &str, +) -> bool { + constructor_names_match(pat_ctor, value_ctor) +} + +fn constructor_names_match(lhs: &str, rhs: &str) -> bool { + // Constructor names are canonicalized to `{Adt}_{Ctor}` (or the builtin + // spelling) at lowering time; suffix-based fuzzy matching is unsound + // because user constructor names may themselves contain underscores + // (`D.Suf` must not fold as `D.Pre_Suf`). + lhs.replace('.', "_") == rhs.replace('.', "_") +} + +fn literal_matches(lit: &LitKind, value: &MonoExpr<'_>) -> bool { + match lit { + LitKind::Number(_) | LitKind::Hex(_) => { + literal_bigint(lit).is_some_and(|lhs| known_int(value).is_some_and(|rhs| lhs == rhs)) + } + LitKind::String(text) => known_string(value) + .is_some_and(|rhs| decode_string_lit(text).is_some_and(|lhs| lhs == rhs)), + LitKind::Error => false, + } +} + +fn literal_bigint(lit: &LitKind) -> Option { + match lit { + LitKind::Number(text) => BigInt::from_decimal_str(text), + LitKind::Hex(text) => BigInt::from_hex_str(text), + LitKind::String(_) | LitKind::Error => None, + } +} + +pub(super) fn remove_assigned<'db>(mut env: VEnv<'db>, assigned: &AssignedNames) -> VEnv<'db> { + match assigned { + AssignedNames::All => env.clear(), + AssignedNames::Names(names) => { + for name in names { + env.remove(name); + } + } + } + env +} + +pub(super) fn remove_comptime_assigned(mut env: CEnv, assigned: &AssignedNames) -> CEnv { + match assigned { + AssignedNames::All => env.clear(), + AssignedNames::Names(names) => { + for name in names { + env.remove(name); + } + } + } + env +} + +pub(super) fn lvalue_root_name(expr: &MonoExpr<'_>) -> Option { + match &expr.kind { + MonoExprKind::Var(id) => Some(id.name.clone()), + MonoExprKind::Index { base, .. } + | MonoExprKind::StorageIndex { base, .. } + | MonoExprKind::Field { base, .. } => lvalue_root_name(base), + _ => None, + } +} + +pub(super) fn collect_pat_binders(pat: &MonoPat<'_>, out: &mut FxHashSet) { + PatBinderCollector { out }.visit_pat(pat); +} + +struct PatBinderCollector<'out> { + out: &'out mut FxHashSet, +} + +impl<'out, 'db> Visitor<'db> for PatBinderCollector<'out> { + fn visit_pat(&mut self, pat: &MonoPat<'db>) { + if let MonoPatKind::Var(id) = &pat.kind { + self.out.insert(id.name.clone()); + } + walk_pat(self, pat); + } + + fn visit_expr(&mut self, _expr: &MonoExpr<'db>) {} +} + +fn decode_string_lit(text: &str) -> Option { + let inner = text.strip_prefix('"')?.strip_suffix('"')?; + let mut out = String::new(); + let mut chars = inner.chars(); + while let Some(ch) = chars.next() { + if ch != '\\' { + out.push(ch); + continue; + } + match chars.next()? { + '"' => out.push('"'), + '\\' => out.push('\\'), + 'n' => out.push('\n'), + 'r' => out.push('\r'), + 't' => out.push('\t'), + other => out.push(other), + } + } + Some(out) +} + +fn encode_string_lit(value: &str) -> String { + let mut out = String::from("\""); + for ch in value.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + ch => out.push(ch), + } + } + out.push('"'); + out +} diff --git a/crates/specialize/src/evaluate/mod.rs b/crates/specialize/src/evaluate/mod.rs new file mode 100644 index 00000000..d4a9ba4e --- /dev/null +++ b/crates/specialize/src/evaluate/mod.rs @@ -0,0 +1,61 @@ +mod assigned; +mod core; +mod dead_code; +mod effects; +mod erasure; +mod known; +mod value; +mod yul_const; + +use hir::nameres::ident_text; +use hir_ty::Db; +use rustc_hash::{FxHashMap, FxHashSet}; + +use self::{core::Evaluator, dead_code::eliminate_dead_functions, value::BigInt}; +use crate::{ + ir::{MonoExpr, MonoId, MonoItem, MonoModule}, + specialize::{SpecializeDiagnostic, SpecializeDiagnosticKind}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct EvaluateOptions { + pub fuel: usize, + pub inline_depth: usize, +} + +pub(crate) fn evaluate_module<'db>( + db: &'db dyn Db, + mut module: MonoModule<'db>, + options: EvaluateOptions, +) -> (MonoModule<'db>, Vec>) { + let mut evaluator = Evaluator::new(db, &module, options.fuel, options.inline_depth); + let mut items = Vec::with_capacity(module.items.len()); + for item in module.items { + match item { + MonoItem::Function(function) => { + items.push(MonoItem::Function(evaluator.eval_function(function))); + } + item => items.push(item), + } + } + module.items = items; + module = eliminate_dead_functions(module); + if !evaluator.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic.kind, + SpecializeDiagnosticKind::ComptimeEvaluationFailed { .. } + | SpecializeDiagnosticKind::ComptimeFuelExhausted { .. } + | SpecializeDiagnosticKind::ComptimeRecursion { .. } + | SpecializeDiagnosticKind::ReductionRecursion { .. } + | SpecializeDiagnosticKind::ReductionFuelExhausted { .. } + ) + }) { + evaluator.check_integer_erasure(&module); + } + (module, evaluator.diagnostics) +} + +type VEnv<'db> = FxHashMap>; +type CEnv = FxHashSet; +type TypeReg<'db> = FxHashMap>; +type YulState = FxHashMap; diff --git a/crates/specialize/src/evaluate/value.rs b/crates/specialize/src/evaluate/value.rs new file mode 100644 index 00000000..5a61d718 --- /dev/null +++ b/crates/specialize/src/evaluate/value.rs @@ -0,0 +1,543 @@ +use std::cmp::Ordering; + +pub(super) fn word_div(lhs: BigInt, rhs: BigInt) -> BigInt { + let lhs = lhs.mod_word(); + let rhs = rhs.mod_word(); + if rhs.is_zero() { + BigInt::zero() + } else { + lhs.div_rem_nonnegative(&rhs) + .map_or(BigInt::zero(), |(q, _)| q) + } +} + +pub(super) fn word_mod(lhs: BigInt, rhs: BigInt) -> BigInt { + let lhs = lhs.mod_word(); + let rhs = rhs.mod_word(); + if rhs.is_zero() { + BigInt::zero() + } else { + lhs.div_rem_nonnegative(&rhs) + .map_or(BigInt::zero(), |(_, r)| r) + } +} + +pub(super) fn word_low_byte(value: &BigInt) -> u8 { + value.mod_word().limbs.first().copied().unwrap_or(0) as u8 +} + +pub(super) fn bitand_word(lhs: &BigInt, rhs: &BigInt) -> BigInt { + word_bitwise(lhs, rhs, |a, b| a & b) +} + +pub(super) fn bitor_word(lhs: &BigInt, rhs: &BigInt) -> BigInt { + word_bitwise(lhs, rhs, |a, b| a | b) +} + +pub(super) fn bitxor_word(lhs: &BigInt, rhs: &BigInt) -> BigInt { + word_bitwise(lhs, rhs, |a, b| a ^ b) +} + +pub(super) fn not_word(value: &BigInt) -> BigInt { + let mut limbs = value.word_limbs(); + for limb in &mut limbs { + *limb = !*limb; + } + BigInt::from_word_limbs(limbs) +} + +pub(super) fn shl_word(value: &BigInt, shift: &BigInt) -> BigInt { + let Some(shift) = shift.mod_word().to_usize_limit(256) else { + return BigInt::zero(); + }; + if shift >= 256 { + BigInt::zero() + } else { + value.mod_word().shl_bits(shift).mod_word() + } +} + +pub(super) fn shr_word(value: &BigInt, shift: &BigInt) -> BigInt { + let Some(shift) = shift.mod_word().to_usize_limit(256) else { + return BigInt::zero(); + }; + if shift >= 256 { + BigInt::zero() + } else { + value.mod_word().shr_bits(shift) + } +} + +fn word_bitwise(lhs: &BigInt, rhs: &BigInt, f: impl Fn(u32, u32) -> u32) -> BigInt { + let lhs = lhs.word_limbs(); + let rhs = rhs.word_limbs(); + let mut out = [0u32; 8]; + for index in 0..8 { + out[index] = f(lhs[index], rhs[index]); + } + BigInt::from_word_limbs(out) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct BigInt { + sign: i8, + limbs: Vec, +} + +impl PartialOrd for BigInt { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for BigInt { + fn cmp(&self, other: &Self) -> Ordering { + match self.sign.cmp(&other.sign) { + Ordering::Equal if self.sign < 0 => other.cmp_abs(self), + Ordering::Equal => self.cmp_abs(other), + order => order, + } + } +} + +impl BigInt { + fn zero() -> Self { + Self { + sign: 0, + limbs: Vec::new(), + } + } + + pub(super) fn from_u64(value: u64) -> Self { + if value == 0 { + return Self::zero(); + } + let mut limbs = vec![value as u32]; + let hi = (value >> 32) as u32; + if hi != 0 { + limbs.push(hi); + } + Self { sign: 1, limbs } + } + + pub(super) fn from_decimal_str(text: &str) -> Option { + let (negative, digits) = text + .strip_prefix('-') + .map_or((false, text), |rest| (true, rest)); + if digits.is_empty() { + return None; + } + let mut value = Self::zero(); + for ch in digits.chars() { + let digit = ch.to_digit(10)?; + value = value.mul_small(10).add_small(digit); + } + if negative && !value.is_zero() { + value.sign = -1; + } + Some(value) + } + + pub(super) fn from_hex_str(text: &str) -> Option { + let digits = text + .strip_prefix("0x") + .or_else(|| text.strip_prefix("0X")) + .unwrap_or(text); + if digits.is_empty() { + return None; + } + let mut value = Self::zero(); + for ch in digits.chars() { + let digit = ch.to_digit(16)?; + value = value.mul_small(16).add_small(digit); + } + Some(value) + } + + pub(super) fn from_be_bytes(bytes: &[u8]) -> Self { + let mut value = Self::zero(); + for byte in bytes { + value = value.mul_small(256).add_small(u32::from(*byte)); + } + value + } + + fn from_word_limbs(limbs: [u32; 8]) -> Self { + let mut out = Self { + sign: 1, + limbs: limbs.to_vec(), + }; + out.normalize(); + out + } + + pub(super) fn is_zero(&self) -> bool { + self.sign == 0 + } + + fn normalize(&mut self) { + while self.limbs.last().is_some_and(|limb| *limb == 0) { + self.limbs.pop(); + } + if self.limbs.is_empty() { + self.sign = 0; + } + } + + fn cmp_abs(&self, other: &Self) -> Ordering { + match self.limbs.len().cmp(&other.limbs.len()) { + Ordering::Equal => self.limbs.iter().rev().cmp(other.limbs.iter().rev()), + order => order, + } + } + + pub(super) fn add(&self, other: &Self) -> Self { + match (self.sign, other.sign) { + (0, _) => other.clone(), + (_, 0) => self.clone(), + (a, b) if a == b => { + let mut out = Self { + sign: self.sign, + limbs: add_abs(&self.limbs, &other.limbs), + }; + out.normalize(); + out + } + _ => match self.cmp_abs(other) { + Ordering::Greater => { + let mut out = Self { + sign: self.sign, + limbs: sub_abs(&self.limbs, &other.limbs), + }; + out.normalize(); + out + } + Ordering::Less => { + let mut out = Self { + sign: other.sign, + limbs: sub_abs(&other.limbs, &self.limbs), + }; + out.normalize(); + out + } + Ordering::Equal => Self::zero(), + }, + } + } + + pub(super) fn sub(&self, other: &Self) -> Self { + self.add(&other.neg()) + } + + fn neg(&self) -> Self { + let mut out = self.clone(); + out.sign = -out.sign; + out + } + + pub(super) fn mul(&self, other: &Self) -> Self { + if self.is_zero() || other.is_zero() { + return Self::zero(); + } + let mut limbs = vec![0u32; self.limbs.len() + other.limbs.len()]; + for (i, &a) in self.limbs.iter().enumerate() { + let mut carry = 0u64; + for (j, &b) in other.limbs.iter().enumerate() { + let idx = i + j; + let acc = u64::from(limbs[idx]) + u64::from(a) * u64::from(b) + carry; + limbs[idx] = acc as u32; + carry = acc >> 32; + } + if carry != 0 { + limbs[i + other.limbs.len()] = carry as u32; + } + } + let mut out = Self { + sign: self.sign * other.sign, + limbs, + }; + out.normalize(); + out + } + + fn mul_small(&self, rhs: u32) -> Self { + if self.is_zero() || rhs == 0 { + return Self::zero(); + } + let mut limbs = Vec::with_capacity(self.limbs.len() + 1); + let mut carry = 0u64; + for &limb in &self.limbs { + let acc = u64::from(limb) * u64::from(rhs) + carry; + limbs.push(acc as u32); + carry = acc >> 32; + } + if carry != 0 { + limbs.push(carry as u32); + } + let mut out = Self { + sign: self.sign, + limbs, + }; + out.normalize(); + out + } + + fn add_small(&self, rhs: u32) -> Self { + self.add(&Self::from_u64(u64::from(rhs))) + } + + fn div_rem_small(&self, rhs: u32) -> (Self, u32) { + assert!(rhs != 0); + if self.is_zero() { + return (Self::zero(), 0); + } + let mut limbs = vec![0u32; self.limbs.len()]; + let mut rem = 0u64; + for (index, &limb) in self.limbs.iter().enumerate().rev() { + let cur = (rem << 32) | u64::from(limb); + limbs[index] = (cur / u64::from(rhs)) as u32; + rem = cur % u64::from(rhs); + } + let mut out = Self { + sign: self.sign, + limbs, + }; + out.normalize(); + (out, rem as u32) + } + + pub(super) fn to_decimal_string(&self) -> String { + if self.is_zero() { + return "0".to_owned(); + } + let mut value = self.abs(); + let mut parts = Vec::new(); + while !value.is_zero() { + let (next, rem) = value.div_rem_small(1_000_000_000); + parts.push(rem); + value = next; + } + let mut out = if self.sign < 0 { + "-".to_owned() + } else { + String::new() + }; + if let Some(last) = parts.pop() { + out.push_str(&last.to_string()); + } + for part in parts.iter().rev() { + out.push_str(&format!("{part:09}")); + } + out + } + + fn abs(&self) -> Self { + let mut out = self.clone(); + if out.sign < 0 { + out.sign = 1; + } + out + } + + pub(super) fn mod_word(&self) -> Self { + if self.sign >= 0 { + return self.lower_256(); + } + let rem = self.abs().lower_256(); + if rem.is_zero() { + Self::zero() + } else { + two_pow_256().sub(&rem) + } + } + + fn lower_256(&self) -> Self { + let mut limbs = self.limbs.iter().copied().take(8).collect::>(); + while limbs.last().is_some_and(|limb| *limb == 0) { + limbs.pop(); + } + if limbs.is_empty() { + Self::zero() + } else { + Self { sign: 1, limbs } + } + } + + fn word_limbs(&self) -> [u32; 8] { + let value = self.mod_word(); + let mut limbs = [0u32; 8]; + for (index, limb) in value.limbs.iter().copied().take(8).enumerate() { + limbs[index] = limb; + } + limbs + } + + pub(super) fn to_word_be_bytes(&self) -> [u8; 32] { + let limbs = self.word_limbs(); + let mut out = [0u8; 32]; + for i in 0..32 { + let limb = limbs[7 - (i / 4)]; + out[i] = ((limb >> (8 * (3 - (i % 4)))) & 0xff) as u8; + } + out + } + + fn shl_bits(&self, bits: usize) -> Self { + if self.is_zero() { + return Self::zero(); + } + let limb_shift = bits / 32; + let bit_shift = bits % 32; + let mut limbs = vec![0u32; limb_shift]; + let mut carry = 0u64; + for &limb in &self.limbs { + let value = (u64::from(limb) << bit_shift) | carry; + limbs.push(value as u32); + carry = value >> 32; + } + if carry != 0 { + limbs.push(carry as u32); + } + let mut out = Self { + sign: self.sign, + limbs, + }; + out.normalize(); + out + } + + fn shr_bits(&self, bits: usize) -> Self { + if self.is_zero() { + return Self::zero(); + } + let limb_shift = bits / 32; + if limb_shift >= self.limbs.len() { + return Self::zero(); + } + let bit_shift = bits % 32; + let mut limbs = Vec::with_capacity(self.limbs.len() - limb_shift); + let mut carry = 0u32; + for &limb in self.limbs[limb_shift..].iter().rev() { + let value = if bit_shift == 0 { + limb + } else { + (limb >> bit_shift) | (carry << (32 - bit_shift)) + }; + limbs.push(value); + carry = limb; + } + limbs.reverse(); + let mut out = Self { + sign: self.sign, + limbs, + }; + out.normalize(); + out + } + + fn bit_len(&self) -> usize { + let Some(last) = self.limbs.last() else { + return 0; + }; + 32 * (self.limbs.len() - 1) + (32 - last.leading_zeros() as usize) + } + + fn bit(&self, index: usize) -> bool { + let limb = index / 32; + let bit = index % 32; + self.limbs + .get(limb) + .is_some_and(|value| (value & (1u32 << bit)) != 0) + } + + fn set_bit(&mut self, index: usize) { + let limb = index / 32; + let bit = index % 32; + if self.limbs.len() <= limb { + self.limbs.resize(limb + 1, 0); + } + self.limbs[limb] |= 1u32 << bit; + if self.sign == 0 { + self.sign = 1; + } + } + + fn div_rem_nonnegative(&self, rhs: &Self) -> Option<(Self, Self)> { + if self.sign < 0 || rhs.sign <= 0 { + return None; + } + if self < rhs { + return Some((Self::zero(), self.clone())); + } + let mut quotient = Self::zero(); + let mut rem = Self::zero(); + for bit in (0..self.bit_len()).rev() { + rem = rem.shl_bits(1); + if self.bit(bit) { + rem = rem.add_small(1); + } + if rem >= *rhs { + rem = rem.sub(rhs); + quotient.set_bit(bit); + } + } + Some((quotient, rem)) + } + + fn to_usize_limit(&self, limit: usize) -> Option { + if self.sign < 0 { + return None; + } + let mut out = 0usize; + for (index, &limb) in self.limbs.iter().enumerate() { + if index >= usize::BITS as usize / 32 { + return None; + } + out |= (limb as usize) << (32 * index); + if out > limit { + return None; + } + } + Some(out) + } +} + +fn add_abs(lhs: &[u32], rhs: &[u32]) -> Vec { + let len = lhs.len().max(rhs.len()); + let mut out = Vec::with_capacity(len + 1); + let mut carry = 0u64; + for index in 0..len { + let acc = u64::from(lhs.get(index).copied().unwrap_or(0)) + + u64::from(rhs.get(index).copied().unwrap_or(0)) + + carry; + out.push(acc as u32); + carry = acc >> 32; + } + if carry != 0 { + out.push(carry as u32); + } + out +} + +fn sub_abs(lhs: &[u32], rhs: &[u32]) -> Vec { + let mut out = Vec::with_capacity(lhs.len()); + let mut borrow = 0i64; + for (index, &left) in lhs.iter().enumerate() { + let right = i64::from(rhs.get(index).copied().unwrap_or(0)); + let mut value = i64::from(left) - right - borrow; + if value < 0 { + value += 1i64 << 32; + borrow = 1; + } else { + borrow = 0; + } + out.push(value as u32); + } + out +} + +fn two_pow_256() -> BigInt { + let mut limbs = vec![0u32; 8]; + limbs.push(1); + BigInt { sign: 1, limbs } +} diff --git a/crates/specialize/src/evaluate/yul_const.rs b/crates/specialize/src/evaluate/yul_const.rs new file mode 100644 index 00000000..89a11001 --- /dev/null +++ b/crates/specialize/src/evaluate/yul_const.rs @@ -0,0 +1,286 @@ +use hir::ast::function::{LitKind, YulExpr, YulExprKind, YulLitKind, YulStmt, YulStmtKind}; +use hir_ty::Db; +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::{ + TypeReg, VEnv, YulState, ident_text, + known::{int_expr, known_int}, + value::{ + BigInt, bitand_word, bitor_word, bitxor_word, not_word, shl_word, shr_word, word_div, + word_mod, + }, +}; +use crate::ir::{MonoExpr, MonoExprKind}; + +pub(super) fn asm_is_interpretable<'db>(db: &'db dyn Db, body: &[YulStmt<'db>]) -> bool { + body.iter().all(|stmt| match &stmt.kind { + YulStmtKind::Assign { names, value } if names.len() == 1 => { + yul_expr_is_interpretable(db, value) + } + YulStmtKind::Expr(YulExpr { + kind: YulExprKind::Call { name, args }, + .. + }) if ["mstore", "mstore8"].contains(&ident_text(db, name).as_str()) && args.len() == 2 => { + args.iter().all(|arg| yul_expr_is_interpretable(db, arg)) + } + _ => false, + }) +} + +fn yul_expr_is_interpretable<'db>(db: &'db dyn Db, expr: &YulExpr<'db>) -> bool { + match &expr.kind { + YulExprKind::Ident(_) => true, + YulExprKind::Lit(YulLitKind::Number(_) | YulLitKind::Hex(_) | YulLitKind::Bool(_)) => true, + YulExprKind::Call { name, args } => { + let name = ident_text(db, name); + (name == "mload" && args.len() == 1 || yul_op_is_interpretable(&name, args.len())) + && args.iter().all(|arg| yul_expr_is_interpretable(db, arg)) + } + YulExprKind::Lit(YulLitKind::String(_) | YulLitKind::Error) | YulExprKind::Error => false, + } +} + +fn yul_op_is_interpretable(name: &str, arity: usize) -> bool { + matches!( + (name, arity), + ("add", 2) + | ("sub", 2) + | ("mul", 2) + | ("div", 2) + | ("mod", 2) + | ("gt", 2) + | ("lt", 2) + | ("eq", 2) + | ("iszero", 1) + | ("and", 2) + | ("or", 2) + | ("xor", 2) + | ("not", 1) + | ("shl", 2) + | ("shr", 2) + ) +} + +pub(super) fn venv_to_yul_state(env: &VEnv<'_>) -> YulState { + env.iter() + .filter_map(|(name, expr)| known_int(expr).map(|value| (name.clone(), value))) + .collect() +} + +pub(super) fn venv_to_yul_subst<'db>( + db: &'db dyn Db, + env: &VEnv<'db>, +) -> FxHashMap> { + env.iter() + .filter_map(|(name, expr)| { + yul_lit_from_known_expr(db, expr).map(|expr| (name.clone(), expr)) + }) + .collect() +} + +pub(super) fn yul_written_names<'db>(db: &'db dyn Db, body: &[YulStmt<'db>]) -> FxHashSet { + let mut names = FxHashSet::default(); + collect_yul_written_names(db, body, &mut names); + names +} + +fn collect_yul_written_names<'db>( + db: &'db dyn Db, + body: &[YulStmt<'db>], + names: &mut FxHashSet, +) { + for stmt in body { + match &stmt.kind { + YulStmtKind::Let { names: bound, .. } | YulStmtKind::Assign { names: bound, .. } => { + names.extend(bound.iter().map(|name| ident_text(db, name))); + } + YulStmtKind::Block(body) | YulStmtKind::If { body, .. } => { + collect_yul_written_names(db, body, names); + } + YulStmtKind::For { + init, post, body, .. + } => { + collect_yul_written_names(db, init, names); + collect_yul_written_names(db, post, names); + collect_yul_written_names(db, body, names); + } + YulStmtKind::Switch { cases, default, .. } => { + for case in cases { + collect_yul_written_names(db, &case.body, names); + } + if let Some(default) = default { + collect_yul_written_names(db, default, names); + } + } + YulStmtKind::FunctionDef { + params, rets, body, .. + } => { + names.extend(params.iter().map(|name| ident_text(db, name))); + names.extend(rets.iter().map(|name| ident_text(db, name))); + collect_yul_written_names(db, body, names); + } + YulStmtKind::Expr(_) + | YulStmtKind::Leave + | YulStmtKind::Break + | YulStmtKind::Continue + | YulStmtKind::Error => {} + } + } +} + +fn yul_lit_from_known_expr<'db>(db: &'db dyn Db, expr: &MonoExpr<'db>) -> Option> { + let span = expr.span; + let lit = match &expr.kind { + MonoExprKind::Lit(LitKind::Number(text)) => YulLitKind::Number(text.clone()), + MonoExprKind::Lit(LitKind::Hex(text)) => YulLitKind::Hex(text.clone()), + MonoExprKind::Lit(LitKind::String(text)) => YulLitKind::String(text.clone()), + MonoExprKind::Conversion { expr, .. } => return yul_lit_from_known_expr(db, expr), + _ => return None, + }; + let _ = db; + Some(YulExpr { + span, + kind: YulExprKind::Lit(lit), + }) +} + +pub(super) fn subst_yul_block<'db>( + db: &'db dyn Db, + subst: &FxHashMap>, + body: Vec>, +) -> Vec> { + body.into_iter() + .map(|stmt| subst_yul_stmt(db, subst, stmt)) + .collect() +} + +fn subst_yul_stmt<'db>( + db: &'db dyn Db, + subst: &FxHashMap>, + stmt: YulStmt<'db>, +) -> YulStmt<'db> { + let span = stmt.span; + let kind = match stmt.kind { + YulStmtKind::Block(body) => YulStmtKind::Block(subst_yul_block(db, subst, body)), + YulStmtKind::Let { names, init } => YulStmtKind::Let { + names, + init: init.map(|expr| subst_yul_expr(db, subst, expr)), + }, + YulStmtKind::Assign { names, value } => YulStmtKind::Assign { + names, + value: subst_yul_expr(db, subst, value), + }, + YulStmtKind::Expr(expr) => YulStmtKind::Expr(subst_yul_expr(db, subst, expr)), + YulStmtKind::If { cond, body } => YulStmtKind::If { + cond: subst_yul_expr(db, subst, cond), + body: subst_yul_block(db, subst, body), + }, + YulStmtKind::For { + init, + cond, + post, + body, + } => YulStmtKind::For { + init: subst_yul_block(db, subst, init), + cond: subst_yul_expr(db, subst, cond), + post: subst_yul_block(db, subst, post), + body: subst_yul_block(db, subst, body), + }, + YulStmtKind::Switch { + expr, + cases, + default, + } => YulStmtKind::Switch { + expr: subst_yul_expr(db, subst, expr), + cases: cases + .into_iter() + .map(|case| hir::ast::function::YulCase { + span: case.span, + lit: case.lit, + body: subst_yul_block(db, subst, case.body), + }) + .collect(), + default: default.map(|body| subst_yul_block(db, subst, body)), + }, + YulStmtKind::FunctionDef { + name, + params, + rets, + body, + } => YulStmtKind::FunctionDef { + name, + params, + rets, + body: subst_yul_block(db, subst, body), + }, + YulStmtKind::Leave => YulStmtKind::Leave, + YulStmtKind::Break => YulStmtKind::Break, + YulStmtKind::Continue => YulStmtKind::Continue, + YulStmtKind::Error => YulStmtKind::Error, + }; + YulStmt { span, kind } +} + +fn subst_yul_expr<'db>( + db: &'db dyn Db, + subst: &FxHashMap>, + expr: YulExpr<'db>, +) -> YulExpr<'db> { + match expr.kind { + YulExprKind::Ident(name) => subst + .get(&ident_text(db, &name)) + .cloned() + .unwrap_or(YulExpr { + span: expr.span, + kind: YulExprKind::Ident(name), + }), + YulExprKind::Call { name, args } => YulExpr { + span: expr.span, + kind: YulExprKind::Call { + name, + args: args + .into_iter() + .map(|arg| subst_yul_expr(db, subst, arg)) + .collect(), + }, + }, + kind => YulExpr { + span: expr.span, + kind, + }, + } +} + +pub(super) fn merge_yul_state<'db>( + type_reg: &TypeReg<'db>, + state: YulState, + mut env: VEnv<'db>, +) -> VEnv<'db> { + for (name, value) in state { + if let Some(id) = type_reg.get(&name) { + env.insert(name, int_expr(value, id.ty, id.span)); + } + } + env +} + +pub(super) fn eval_yul_op(name: &str, values: &[BigInt]) -> Option { + match (name, values) { + ("add", [a, b]) => Some(a.add(b).mod_word()), + ("sub", [a, b]) => Some(a.sub(b).mod_word()), + ("mul", [a, b]) => Some(a.mul(b).mod_word()), + ("div", [a, b]) => Some(word_div(a.clone(), b.clone())), + ("mod", [a, b]) => Some(word_mod(a.clone(), b.clone())), + ("gt", [a, b]) => Some(BigInt::from_u64(u64::from(a.mod_word() > b.mod_word()))), + ("lt", [a, b]) => Some(BigInt::from_u64(u64::from(a.mod_word() < b.mod_word()))), + ("eq", [a, b]) => Some(BigInt::from_u64(u64::from(a.mod_word() == b.mod_word()))), + ("iszero", [a]) => Some(BigInt::from_u64(u64::from(a.mod_word().is_zero()))), + ("and", [a, b]) => Some(bitand_word(a, b)), + ("or", [a, b]) => Some(bitor_word(a, b)), + ("xor", [a, b]) => Some(bitxor_word(a, b)), + ("not", [a]) => Some(not_word(a)), + ("shl", [sh, value]) => Some(shl_word(value, sh)), + ("shr", [sh, value]) => Some(shr_word(value, sh)), + _ => None, + } +} diff --git a/crates/specialize/src/ir.rs b/crates/specialize/src/ir.rs new file mode 100644 index 00000000..bc448ef0 --- /dev/null +++ b/crates/specialize/src/ir.rs @@ -0,0 +1,484 @@ +use hir::{ + anchor::DefId, + ast::function::{AssignOp, BinOp, LitKind, UnOp, YulStmt}, + span::Span, +}; +use hir_ty::{AbiType, BuiltinTyCtor, ConversionKind, FrontendDesugarPlan, Ty, TyCtor, TyKind}; + +pub(crate) mod visit; + +/// A semantic type that has been checked to contain no type variables or +/// unknown placeholders by the specializer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MonoTy<'db> { + ty: Ty<'db>, +} + +impl<'db> MonoTy<'db> { + pub(crate) fn new_unchecked(ty: Ty<'db>) -> Self { + Self { ty } + } + + /// Returns the underlying semantic type. + pub fn ty(self) -> Ty<'db> { + self.ty + } +} + +/// Name plus concrete type. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MonoId<'db> { + pub name: String, + pub ty: MonoTy<'db>, + pub span: Span<'db>, +} + +impl<'db> MonoId<'db> { + /// Returns the builtin constructor identity represented by this id, when + /// both the backend name and the semantic result type agree. + pub fn builtin_ctor(&self, db: &'db dyn hir_ty::Db) -> Option { + MonoBuiltinCtor::from_name(&self.name).filter(|ctor| ctor.matches_result_ty(db, self.ty.ty)) + } + + /// Checks whether this id is the given builtin constructor. + pub fn is_builtin_ctor(&self, db: &'db dyn hir_ty::Db, ctor: MonoBuiltinCtor) -> bool { + self.builtin_ctor(db) == Some(ctor) + } +} + +/// Builtin constructor identities carried in mono IR by name plus semantic type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MonoBuiltinCtor { + Unit, + Pair, + True, + False, + Inl, + Inr, +} + +impl MonoBuiltinCtor { + pub fn name(self) -> &'static str { + match self { + Self::Unit => "()", + Self::Pair => "pair", + Self::True => "true", + Self::False => "false", + Self::Inl => "inl", + Self::Inr => "inr", + } + } + + pub fn from_name(name: &str) -> Option { + match name { + "()" => Some(Self::Unit), + "pair" => Some(Self::Pair), + "true" => Some(Self::True), + "false" => Some(Self::False), + "inl" => Some(Self::Inl), + "inr" => Some(Self::Inr), + _ => None, + } + } + + fn matches_result_ty<'db>(self, db: &'db dyn hir_ty::Db, ty: Ty<'db>) -> bool { + let expected = self.result_ty_ctor(); + matches!( + ty.kind(db), + TyKind::Named { + ctor: TyCtor::Builtin(actual), + .. + } if *actual == expected + ) + } + + fn result_ty_ctor(self) -> BuiltinTyCtor { + match self { + Self::Unit => BuiltinTyCtor::Unit, + Self::Pair => BuiltinTyCtor::Pair, + Self::True | Self::False => BuiltinTyCtor::Bool, + Self::Inl | Self::Inr => BuiltinTyCtor::Sum, + } + } +} + +/// Intrinsic call that may be folded by the evaluator. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MonoIntrinsic { + PrimAddWord, + PrimEqWord, + SubWord, + MulWord, + GtWord, + BxorWord, + BandWord, + BorWord, + WordToInteger, + WordFromInteger, + IntegerAdd, + IntegerSub, + IntegerMul, + IntegerLt, + IntegerEq, + ConcatLit, + StrlenLit, + KeccakLit, +} + +/// Resolved origin for a monomorphic call expression. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MonoCallOrigin<'db> { + Source(DefId<'db>), + Builtin(MonoIntrinsic), + /// Call resolved to a backend name only (no source DefId or builtin + /// intrinsic): resolved operator overloads, evidence-resolved class + /// methods/invokables, int fromInteger, builtins without an intrinsic, + /// and closure-dispatch to a known function. + ByName, +} + +/// Specialized module. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoModule<'db> { + pub module: DefId<'db>, + pub frontend_desugar: FrontendDesugarPlan<'db>, + /// Specialized function names that form this compilation unit's external roots. + pub entry_points: Vec, + pub items: Vec>, +} + +/// Top-level monomorphic item. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MonoItem<'db> { + Contract(MonoContract<'db>), + Function(MonoFunction<'db>), + Adt(DefId<'db>), +} + +/// Contract entry summary and specialized functions reachable from its dispatch +/// surface. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoContract<'db> { + pub def: DefId<'db>, + pub name: String, + pub span: Span<'db>, + pub constructor: MonoConstructor<'db>, + pub fallback: MonoFallback<'db>, + pub entries: Vec>, +} + +/// One dispatch entry and its concrete specialized function name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MonoEntry<'db> { + SelectorMethod { + source: DefId<'db>, + name: String, + specialized: String, + span: Span<'db>, + selector: [u8; 4], + signature: String, + payable: bool, + inputs: Vec, + outputs: Vec, + }, + /// Compiler-generated deployment entry produced by the constructor HIR overlay. + DeploymentMain { + source: DefId<'db>, + specialized: String, + span: Span<'db>, + }, + Fallback { + source: DefId<'db>, + specialized: String, + span: Span<'db>, + payable: bool, + inputs: Vec, + outputs: Vec, + }, + RuntimeMain { + source: DefId<'db>, + specialized: String, + span: Span<'db>, + origin: MonoRuntimeMainOrigin, + }, +} + +/// Provenance of the contract runtime entry selected by specialization. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MonoRuntimeMainOrigin { + /// An ordinary `main` written by the user, irrespective of visibility. + User, + /// A compiler-owned wrapper whose semantics come from `std.dispatch`. + StdDispatch, +} + +/// Source constructor ABI metadata. Constructor execution is rooted exclusively +/// through [`MonoEntry::DeploymentMain`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoConstructor<'db> { + pub source: Option>, + pub explicit: bool, + pub payable: bool, + pub inputs: Vec, + pub span: Span<'db>, +} + +/// Fallback dispatch/ABI metadata. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoFallback<'db> { + pub source: Option>, + pub explicit: bool, + pub specialized: Option, + pub payable: bool, + pub inputs: Vec, + pub outputs: Vec, + pub span: Span<'db>, +} + +/// ABI parameter or tuple component. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MonoAbiParam { + pub name: String, + pub ty: AbiType, + pub components: Vec, +} + +/// Specialized function. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoFunction<'db> { + pub origin: MonoFunctionOrigin<'db>, + pub source: Option>, + pub shadowed_top_level: Option, + pub name: String, + pub span: Span<'db>, + pub params: Vec>, + pub ret: MonoTy<'db>, + pub comptime_obligations: Vec>, + pub body: Vec>, +} + +/// Provenance for a specialized function. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MonoFunctionOrigin<'db> { + Source, + InstanceMethod { + instance: DefId<'db>, + class: String, + method: String, + }, + DerivedGeneric { + adt: DefId<'db>, + method: String, + }, + External, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamMode { + Runtime, + Comptime, +} + +impl ParamMode { + pub fn from_bool(b: bool) -> Self { + if b { Self::Comptime } else { Self::Runtime } + } + + pub fn is_comptime(self) -> bool { + matches!(self, Self::Comptime) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LetMode { + Runtime, + Comptime, +} + +impl LetMode { + pub fn from_bool(b: bool) -> Self { + if b { Self::Comptime } else { Self::Runtime } + } + + pub fn is_comptime(self) -> bool { + matches!(self, Self::Comptime) + } +} + +/// Concrete function parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoParam<'db> { + pub name: String, + pub mode: ParamMode, + pub ty: MonoTy<'db>, + pub span: Span<'db>, +} + +/// A comptime obligation carried from type inference into mono IR. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoComptimeObligation<'db> { + pub span: Span<'db>, + pub expr: MonoExpr<'db>, + pub kind: MonoComptimeObligationKind, +} + +/// Source of a comptime obligation. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MonoComptimeObligationKind { + LetInit { name: String }, + Return { context: String }, + CallParam { function: String, param: String }, + PatternLabel, +} + +/// Specialized statement. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoStmt<'db> { + pub span: Span<'db>, + pub kind: MonoStmtKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MonoStmtKind<'db> { + Let { + mode: LetMode, + id: MonoId<'db>, + ty: Option>, + init: Option>, + }, + Return(Option>), + Expr(MonoExpr<'db>), + Assign { + op: AssignOp, + lhs: MonoExpr<'db>, + rhs: MonoExpr<'db>, + }, + Match { + scrutinees: Vec>, + arms: Vec>, + }, + For { + init: Vec>, + cond: MonoExpr<'db>, + post: Vec>, + body: Vec>, + }, + If { + cond: MonoExpr<'db>, + then_body: Vec>, + else_body: Option>>, + }, + Block(Vec>), + Assembly(Vec>), + Break, + Continue, + Error, +} + +/// Specialized expression. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoExpr<'db> { + pub span: Span<'db>, + pub ty: MonoTy<'db>, + pub kind: MonoExprKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MonoExprKind<'db> { + Var(MonoId<'db>), + Lit(LitKind), + Tuple(Vec>), + Call { + callee: MonoId<'db>, + args: Vec>, + origin: MonoCallOrigin<'db>, + }, + Con { + ctor: MonoId<'db>, + args: Vec>, + }, + ClosureDispatch { + callee: Box>, + args: Vec>, + }, + BinOp { + lhs: Box>, + op: BinOp, + rhs: Box>, + }, + UnaryOp { + op: UnOp, + expr: Box>, + }, + Index { + base: Box>, + index: Box>, + }, + StorageIndex { + base: Box>, + index: Box>, + }, + Field { + base: Box>, + field: String, + }, + Proxy(MonoTy<'db>), + Conversion { + expr: Box>, + ty: MonoTy<'db>, + kind: ConversionKind, + }, + Match { + scrutinee: Box>, + arms: Vec>, + }, + If { + cond: Box>, + then_expr: Box>, + else_expr: Box>, + }, + Lambda { + name: String, + params: Vec>, + body: Vec>, + }, + Error, +} + +/// Specialized match arm. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoArm<'db> { + pub span: Span<'db>, + pub pats: Vec>, + pub body: Vec>, +} + +/// Specialized expression match arm. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoExprArm<'db> { + pub span: Span<'db>, + pub pat: MonoPat<'db>, + pub expr: MonoExpr<'db>, +} + +/// Specialized pattern. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MonoPat<'db> { + pub span: Span<'db>, + pub ty: MonoTy<'db>, + pub kind: MonoPatKind<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MonoPatKind<'db> { + Wildcard, + Var(MonoId<'db>), + Lit(LitKind), + Con { + ctor: MonoId<'db>, + args: Vec>, + }, + Tuple(Vec>), + ComptimeLabel(MonoExpr<'db>), + Error, +} diff --git a/crates/specialize/src/ir/visit.rs b/crates/specialize/src/ir/visit.rs new file mode 100644 index 00000000..679b55bd --- /dev/null +++ b/crates/specialize/src/ir/visit.rs @@ -0,0 +1,167 @@ +use super::{MonoExpr, MonoExprKind, MonoPat, MonoPatKind, MonoStmt, MonoStmtKind}; + +pub(crate) trait Visitor<'db>: Sized { + fn visit_stmt(&mut self, stmt: &MonoStmt<'db>) { + walk_stmt(self, stmt); + } + + fn visit_expr(&mut self, expr: &MonoExpr<'db>) { + walk_expr(self, expr); + } + + fn visit_pat(&mut self, pat: &MonoPat<'db>) { + walk_pat(self, pat); + } +} + +pub(crate) fn walk_stmt<'db, V>(visitor: &mut V, stmt: &MonoStmt<'db>) +where + V: Visitor<'db>, +{ + match &stmt.kind { + MonoStmtKind::Let { init, .. } => { + if let Some(init) = init { + visitor.visit_expr(init); + } + } + MonoStmtKind::Return(expr) => { + if let Some(expr) = expr { + visitor.visit_expr(expr); + } + } + MonoStmtKind::Expr(expr) => visitor.visit_expr(expr), + MonoStmtKind::Assign { lhs, rhs, .. } => { + visitor.visit_expr(lhs); + visitor.visit_expr(rhs); + } + MonoStmtKind::Match { scrutinees, arms } => { + for scrutinee in scrutinees { + visitor.visit_expr(scrutinee); + } + for arm in arms { + for pat in &arm.pats { + visitor.visit_pat(pat); + } + for stmt in &arm.body { + visitor.visit_stmt(stmt); + } + } + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + for stmt in init { + visitor.visit_stmt(stmt); + } + visitor.visit_expr(cond); + for stmt in post { + visitor.visit_stmt(stmt); + } + for stmt in body { + visitor.visit_stmt(stmt); + } + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + visitor.visit_expr(cond); + for stmt in then_body { + visitor.visit_stmt(stmt); + } + if let Some(else_body) = else_body { + for stmt in else_body { + visitor.visit_stmt(stmt); + } + } + } + MonoStmtKind::Block(body) => { + for stmt in body { + visitor.visit_stmt(stmt); + } + } + MonoStmtKind::Assembly(_) + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Error => {} + } +} + +pub(crate) fn walk_expr<'db, V>(visitor: &mut V, expr: &MonoExpr<'db>) +where + V: Visitor<'db>, +{ + match &expr.kind { + MonoExprKind::Tuple(elems) => { + for elem in elems { + visitor.visit_expr(elem); + } + } + MonoExprKind::Call { args, .. } | MonoExprKind::Con { args, .. } => { + for arg in args { + visitor.visit_expr(arg); + } + } + MonoExprKind::ClosureDispatch { callee, args } => { + visitor.visit_expr(callee); + for arg in args { + visitor.visit_expr(arg); + } + } + MonoExprKind::BinOp { lhs, rhs, .. } => { + visitor.visit_expr(lhs); + visitor.visit_expr(rhs); + } + MonoExprKind::UnaryOp { expr, .. } => visitor.visit_expr(expr), + MonoExprKind::Index { base, index } | MonoExprKind::StorageIndex { base, index } => { + visitor.visit_expr(base); + visitor.visit_expr(index); + } + MonoExprKind::Field { base, .. } => visitor.visit_expr(base), + MonoExprKind::Conversion { expr, .. } => visitor.visit_expr(expr), + MonoExprKind::Match { scrutinee, arms } => { + visitor.visit_expr(scrutinee); + for arm in arms { + visitor.visit_pat(&arm.pat); + visitor.visit_expr(&arm.expr); + } + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => { + visitor.visit_expr(cond); + visitor.visit_expr(then_expr); + visitor.visit_expr(else_expr); + } + MonoExprKind::Lambda { body, .. } => { + for stmt in body { + visitor.visit_stmt(stmt); + } + } + MonoExprKind::Var(_) + | MonoExprKind::Lit(_) + | MonoExprKind::Proxy(_) + | MonoExprKind::Error => {} + } +} + +pub(crate) fn walk_pat<'db, V>(visitor: &mut V, pat: &MonoPat<'db>) +where + V: Visitor<'db>, +{ + match &pat.kind { + MonoPatKind::Con { args, .. } | MonoPatKind::Tuple(args) => { + for arg in args { + visitor.visit_pat(arg); + } + } + MonoPatKind::ComptimeLabel(expr) => visitor.visit_expr(expr), + MonoPatKind::Wildcard | MonoPatKind::Var(_) | MonoPatKind::Lit(_) | MonoPatKind::Error => {} + } +} diff --git a/crates/specialize/src/lib.rs b/crates/specialize/src/lib.rs new file mode 100644 index 00000000..69669449 --- /dev/null +++ b/crates/specialize/src/lib.rs @@ -0,0 +1,31 @@ +//! Evidence-driven monomorphization for Solcore HIR. +//! +//! This crate deliberately sits above `hir`, `nameres`, and `hir-ty` instead of +//! inside `hir-ty`: type inference owns evidence production, while later +//! backend stages such as Hull need an evidence-free, monomorphic IR. Keeping +//! the pass in its own crate lets consumers depend on the monomorphic surface +//! without adding backend concerns to type checking. +//! +//! The public entry point is [`specialize_module`]. It starts from a contract's +//! typed dispatch surface or from `main` in non-contract modules, follows local +//! direct calls, resolves class-method call-site evidence to concrete instance +//! methods, and emits a monomorphic IR with concrete semantic types on every +//! node. Imported definitions that are not present in the entry HIR module are +//! preserved as external monomorphic calls; whole-program expansion can layer +//! on top of this crate without changing the IR. + +mod evaluate; +mod ir; +mod specialize; + +pub use ir::{ + LetMode, MonoAbiParam, MonoArm, MonoBuiltinCtor, MonoCallOrigin, MonoComptimeObligation, + MonoComptimeObligationKind, MonoConstructor, MonoContract, MonoEntry, MonoExpr, MonoExprArm, + MonoExprKind, MonoFallback, MonoFunction, MonoFunctionOrigin, MonoId, MonoIntrinsic, MonoItem, + MonoModule, MonoParam, MonoPat, MonoPatKind, MonoRuntimeMainOrigin, MonoStmt, MonoStmtKind, + MonoTy, ParamMode, +}; +pub use specialize::{ + SpecializeDiagnostic, SpecializeDiagnosticKind, SpecializeOptions, SpecializeOutput, + specialize_module, specialize_name, specialize_prepared_module, +}; diff --git a/crates/specialize/src/specialize/body.rs b/crates/specialize/src/specialize/body.rs new file mode 100644 index 00000000..e0e15305 --- /dev/null +++ b/crates/specialize/src/specialize/body.rs @@ -0,0 +1,1372 @@ +use super::*; + +pub(super) struct BodyCtx<'a, 'db> { + pub(super) driver: &'a mut Driver<'db>, + pub(super) info: &'a FunctionInfo<'db>, + pub(super) body: FuncBody<'db>, + pub(super) result: InferenceResult<'db>, + pub(super) body_map: hir_nameres::BodyResolutionMap<'db>, + pub(super) pre_typeck_desugar: Vec>, + pub(super) subst: TySubst<'db>, + pub(super) depth: usize, + pub(super) index: Arc>, + pub(super) lowered_exprs: FxHashMap>, MonoExpr<'db>>, + pub(super) locals: FxHashMap>, +} + +pub(super) struct BodyIndex<'db> { + expr_tys: FxHashMap<(FuncBody<'db>, Id>), Ty<'db>>, + pat_tys: FxHashMap<(FuncBody<'db>, Id>), Ty<'db>>, + let_tys: FxHashMap<(FuncBody<'db>, Id>), Ty<'db>>, + adt_field_indices: FxHashMap<(FuncBody<'db>, Id>), u32>, + checked_conversions: FxHashMap<(FuncBody<'db>, Id>), CheckedConversion<'db>>, + expr_resolutions: FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + pat_resolutions: FxHashMap<(FuncBody<'db>, Id>), hir_nameres::Resolution<'db>>, + call_evidence: FxHashMap<(FuncBody<'db>, Id>, Id>), CallSiteEvidence<'db>>, + class_method_value_evidence: + FxHashMap<(FuncBody<'db>, Id>, DefId<'db>), Evidence<'db>>, + first_builtin_int_evidence: Option>, + comptime_let_stmts: FxHashSet<(FuncBody<'db>, Id>)>, + comptime_obligations: FxHashMap, Vec>>, +} + +impl<'db> BodyIndex<'db> { + pub(super) fn new( + db: &'db dyn hir_ty::Db, + result: &InferenceResult<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + ) -> Self { + let mut index = Self { + expr_tys: FxHashMap::default(), + pat_tys: FxHashMap::default(), + let_tys: FxHashMap::default(), + adt_field_indices: FxHashMap::default(), + checked_conversions: FxHashMap::default(), + expr_resolutions: FxHashMap::default(), + pat_resolutions: FxHashMap::default(), + call_evidence: FxHashMap::default(), + class_method_value_evidence: FxHashMap::default(), + first_builtin_int_evidence: None, + comptime_let_stmts: FxHashSet::default(), + comptime_obligations: FxHashMap::default(), + }; + + for entry in &result.expr_tys { + index + .expr_tys + .entry((entry.body, entry.expr)) + .or_insert(entry.ty); + } + for entry in &result.pat_tys { + index + .pat_tys + .entry((entry.body, entry.pat)) + .or_insert(entry.ty); + } + for entry in &result.let_tys { + index + .let_tys + .entry((entry.body, entry.stmt)) + .or_insert(entry.ty); + } + for selection in &result.adt_field_selections { + index + .adt_field_indices + .entry((selection.body, selection.expr)) + .or_insert(selection.index); + } + for conversion in &result.checked_conversions { + index + .checked_conversions + .entry((conversion.body, conversion.expr)) + .or_insert_with(|| conversion.clone()); + } + for entry in &body_map.exprs { + let key = (entry.body, entry.expr); + match index.expr_resolutions.get_mut(&key) { + Some(current) + if !preferred_expr_resolution(current) + && preferred_expr_resolution(&entry.resolution) => + { + *current = entry.resolution.clone(); + } + Some(_) => {} + None => { + index.expr_resolutions.insert(key, entry.resolution.clone()); + } + } + } + for entry in &body_map.pats { + index + .pat_resolutions + .entry((entry.body, entry.pat)) + .or_insert_with(|| entry.resolution.clone()); + } + for evidence in &result.call_site_evidence { + index + .call_evidence + .entry((evidence.body, evidence.call_expr, evidence.callee_expr)) + .or_insert_with(|| evidence.clone()); + if index.first_builtin_int_evidence.is_none() + && matches!( + evidence.callee, + CallSiteCallee::Builtin(hir_nameres::BuiltinKind::ClassMethod( + hir_nameres::BuiltinClassMethod::IntFromInteger + )) + ) + { + index.first_builtin_int_evidence = Some(evidence.clone()); + } + } + for solved in &result.obligation_evidence { + let Some(obligation) = result.obligations.get(solved.obligation) else { + continue; + }; + let hir_ty::ObligationSource::ClassMethod { body, expr } = obligation.source else { + continue; + }; + let PredKind::InClass { + class: ClassId::User(class), + .. + } = obligation.pred.kind(db) + else { + continue; + }; + index + .class_method_value_evidence + .entry((body, expr, *class)) + .or_insert_with(|| solved.evidence.clone()); + } + for obligation in &result.comptime_obligations { + if let ComptimeObligationKind::LetInit { stmt, .. } = &obligation.kind { + index.comptime_let_stmts.insert((obligation.body, *stmt)); + } + index + .comptime_obligations + .entry(obligation.body) + .or_default() + .push(obligation.clone()); + } + #[cfg(debug_assertions)] + index.debug_assert_complete(result, body_map); + index + } + + /// Keeps the performance property testable without relying on a wall-clock + /// threshold: every hot lookup table must be fully materialized, with one + /// entry per distinct source key and no fallback scan required. + #[cfg(debug_assertions)] + fn debug_assert_complete( + &self, + result: &InferenceResult<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + ) { + let expr_ty_keys = result + .expr_tys + .iter() + .map(|entry| (entry.body, entry.expr)) + .collect::>(); + debug_assert_eq!(self.expr_tys.len(), expr_ty_keys.len()); + debug_assert!( + expr_ty_keys + .iter() + .all(|key| self.expr_tys.contains_key(key)) + ); + + let pat_ty_keys = result + .pat_tys + .iter() + .map(|entry| (entry.body, entry.pat)) + .collect::>(); + debug_assert_eq!(self.pat_tys.len(), pat_ty_keys.len()); + debug_assert!(pat_ty_keys.iter().all(|key| self.pat_tys.contains_key(key))); + + let let_ty_keys = result + .let_tys + .iter() + .map(|entry| (entry.body, entry.stmt)) + .collect::>(); + debug_assert_eq!(self.let_tys.len(), let_ty_keys.len()); + debug_assert!(let_ty_keys.iter().all(|key| self.let_tys.contains_key(key))); + + let adt_field_keys = result + .adt_field_selections + .iter() + .map(|selection| (selection.body, selection.expr)) + .collect::>(); + debug_assert_eq!(self.adt_field_indices.len(), adt_field_keys.len()); + debug_assert!( + adt_field_keys + .iter() + .all(|key| self.adt_field_indices.contains_key(key)) + ); + + let conversion_keys = result + .checked_conversions + .iter() + .map(|conversion| (conversion.body, conversion.expr)) + .collect::>(); + debug_assert_eq!(self.checked_conversions.len(), conversion_keys.len()); + debug_assert!( + conversion_keys + .iter() + .all(|key| self.checked_conversions.contains_key(key)) + ); + + let expr_resolution_keys = body_map + .exprs + .iter() + .map(|entry| (entry.body, entry.expr)) + .collect::>(); + debug_assert_eq!(self.expr_resolutions.len(), expr_resolution_keys.len()); + debug_assert!( + expr_resolution_keys + .iter() + .all(|key| self.expr_resolutions.contains_key(key)) + ); + + let pat_resolution_keys = body_map + .pats + .iter() + .map(|entry| (entry.body, entry.pat)) + .collect::>(); + debug_assert_eq!(self.pat_resolutions.len(), pat_resolution_keys.len()); + debug_assert!( + pat_resolution_keys + .iter() + .all(|key| self.pat_resolutions.contains_key(key)) + ); + + let call_evidence_keys = result + .call_site_evidence + .iter() + .map(|entry| (entry.body, entry.call_expr, entry.callee_expr)) + .collect::>(); + debug_assert_eq!(self.call_evidence.len(), call_evidence_keys.len()); + debug_assert!( + call_evidence_keys + .iter() + .all(|key| self.call_evidence.contains_key(key)) + ); + + let indexed_comptime_obligations = self + .comptime_obligations + .values() + .map(Vec::len) + .sum::(); + debug_assert_eq!( + indexed_comptime_obligations, + result.comptime_obligations.len() + ); + } +} + +fn preferred_expr_resolution(resolution: &hir_nameres::Resolution<'_>) -> bool { + matches!( + resolution, + hir_nameres::Resolution::Def { + kind: hir_nameres::DefResolutionKind::Function | hir_nameres::DefResolutionKind::Class, + .. + } | hir_nameres::Resolution::Builtin(_) + | hir_nameres::Resolution::ClassMethod { .. } + | hir_nameres::Resolution::Ctor { .. } + ) +} + +#[derive(Clone, Copy)] +pub(super) struct BinOpExpr<'db> { + pub(super) expr_id: Id>, + pub(super) lhs: Id>, + pub(super) op: BinOp, + pub(super) rhs: Id>, + pub(super) result_ty: Ty<'db>, + pub(super) span: Span<'db>, +} + +impl<'a, 'db> BodyCtx<'a, 'db> { + pub(super) fn stmt(&mut self, stmt_id: Id>) -> Option> { + let stmt = self.body.stmts(self.driver.db).get(stmt_id); + let span = stmt.span; + let kind = match &stmt.kind { + StmtKind::Let { + comptime, + name, + ty, + init, + } => { + let init_expr = match init { + Some(expr) => Some(self.expr(*expr)?), + None => None, + }; + let annotation_ty = match ty { + Some(ty) => Some(self.lower_body_ty(*ty)?), + None => None, + }; + let sem_ty = self + .index + .let_tys + .get(&(self.body, stmt_id)) + .copied() + .or_else(|| init.and_then(|expr| self.expr_ty(expr)).or(annotation_ty)) + .map(|ty| self.subst.apply_ty(self.driver.db, ty)) + .unwrap_or_else(|| Ty::unknown(self.driver.db)); + let id = MonoId { + name: ident_text(self.driver.db, name), + ty: self.driver.mono_ty(sem_ty, "let binding", span)?, + span: name.span(self.driver.db), + }; + self.locals.insert(id.name.clone(), sem_ty); + let annotation_is_comptime = annotation_ty + .as_ref() + .is_some_and(|ty| ty_is_comptime(self.driver.db, *ty)); + let comptime = comptime.is_some() + || annotation_is_comptime + || self.stmt_has_comptime_let_obligation(stmt_id); + MonoStmtKind::Let { + mode: LetMode::from_bool(comptime), + id, + ty: match annotation_ty { + Some(ty) => { + let ty = self.subst.apply_ty(self.driver.db, ty); + Some(self.driver.mono_ty(ty, "let annotation", span)?) + } + None => None, + }, + init: init_expr, + } + } + StmtKind::Return(expr) => MonoStmtKind::Return(match expr { + Some(expr) => Some(self.expr(*expr)?), + None => None, + }), + StmtKind::Expr(expr) => MonoStmtKind::Expr(self.expr(*expr)?), + StmtKind::Assign { op, lhs, rhs } => MonoStmtKind::Assign { + op: *op, + lhs: self.expr(*lhs)?, + rhs: self.expr(*rhs)?, + }, + StmtKind::Match { scrutinees, arms } => MonoStmtKind::Match { + scrutinees: scrutinees + .iter() + .map(|expr| self.expr(*expr)) + .collect::>>()?, + arms: arms + .iter() + .map(|arm| self.arm(arm)) + .collect::>>()?, + }, + StmtKind::For { + init, + cond, + post, + body, + } => MonoStmtKind::For { + init: init + .iter() + .map(|stmt| self.stmt(*stmt)) + .collect::>>()?, + cond: self.expr(*cond)?, + post: post + .iter() + .map(|stmt| self.stmt(*stmt)) + .collect::>>()?, + body: body + .iter() + .map(|stmt| self.stmt(*stmt)) + .collect::>>()?, + }, + StmtKind::If { + cond, + then_body, + else_body, + } => self.if_stmt(stmt_id, *cond, then_body, else_body.as_deref(), span)?, + StmtKind::Block { body } => MonoStmtKind::Block( + body.iter() + .map(|stmt| self.stmt(*stmt)) + .collect::>>()?, + ), + StmtKind::Assembly { body } => MonoStmtKind::Assembly(body.clone()), + StmtKind::Break => MonoStmtKind::Break, + StmtKind::Continue => MonoStmtKind::Continue, + StmtKind::Error => MonoStmtKind::Error, + }; + Some(MonoStmt { span, kind }) + } + + fn arm(&mut self, arm: &MatchArm<'db>) -> Option> { + Some(MonoArm { + span: arm.span, + pats: arm + .pats + .iter() + .map(|pat| self.pat(*pat)) + .collect::>>()?, + body: arm + .body + .iter() + .map(|stmt| self.stmt(*stmt)) + .collect::>>()?, + }) + } + + pub(super) fn expr(&mut self, expr_id: Id>) -> Option> { + let expr = self.body.exprs(self.driver.db).get(expr_id); + let checked_conversion = if matches!(&expr.kind, ExprKind::Conversion { .. }) { + self.checked_conversion(expr_id).cloned() + } else { + None + }; + let mut ty = self + .expr_ty(expr_id) + .map(|ty| self.subst.apply_ty(self.driver.db, ty)) + .unwrap_or_else(|| Ty::unknown(self.driver.db)); + if matches!(ty.kind(self.driver.db), TyKind::Unknown) + && let ExprKind::Ident(name) = &expr.kind + && let Some(local_ty) = self.locals.get(ident_text(self.driver.db, name).as_str()) + { + ty = *local_ty; + } + if matches!(ty.kind(self.driver.db), TyKind::Unknown) + && let ExprKind::Call { callee, .. } = &expr.kind + && let Some(ctor_ty) = self.constructor_call_result_ty(*callee) + { + ty = ctor_ty; + } + if !ty_is_closed(self.driver.db, ty) + && let ExprKind::Ident(_) = &expr.kind + && let Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + }) = self.expr_resolution(expr_id) + && let Some(fn_ty) = self.function_value_ty(def) + { + ty = fn_ty; + } + if !ty_is_closed(self.driver.db, ty) + && let ExprKind::Call { callee, .. } = &expr.kind + && let Some(ret_ty) = self.invokable_call_result_ty(expr_id, *callee) + { + ty = ret_ty; + } + if !ty_is_closed(self.driver.db, ty) + && let ExprKind::Call { callee, args } = &expr.kind + && let Some(closed) = self.close_method_constructor_ty(ty, *callee, args) + { + ty = closed; + } + match &expr.kind { + ExprKind::Conversion { ty: target, .. } => { + let target = match checked_conversion.as_ref() { + Some(conversion) => conversion.target, + None => self.lower_body_ty(*target)?, + }; + ty = self.subst.apply_ty(self.driver.db, target); + } + ExprKind::TypeAscription { ty: target, .. } => { + let target = self.lower_body_ty(*target)?; + ty = self.subst.apply_ty(self.driver.db, target); + } + _ => {} + } + let mono_ty = self.driver.mono_ty(ty, "expression", expr.span)?; + if let Some(kind) = self.bool_expr_kind(expr_id, mono_ty, expr.span) { + let mono_expr = MonoExpr { + span: expr.span, + ty: mono_ty, + kind, + }; + self.lowered_exprs.insert(expr_id, mono_expr.clone()); + return Some(mono_expr); + } + let kind = match &expr.kind { + ExprKind::Lit(lit) => MonoExprKind::Lit(lit.clone()), + ExprKind::Ident(name) => self.ident_expr(expr_id, name, mono_ty, expr.span), + ExprKind::Tuple(elems) => self.tuple_expr(expr_id, elems, ty, expr.span)?.kind, + ExprKind::Call { callee, args } => { + self.call_expr(expr_id, *callee, args, ty, expr.span)? + } + ExprKind::Field { base, field } => { + if let Some(index) = self.adt_field_index(expr_id) { + MonoExprKind::Field { + base: Box::new(self.expr(*base)?), + field: index.to_string(), + } + } else if let Some(resolution) = self.expr_resolution(expr_id) { + match resolution { + hir_nameres::Resolution::Ctor { ty: adt, index } => MonoExprKind::Con { + ctor: MonoId { + name: ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + ty: mono_ty, + span: expr.span, + }, + args: Vec::new(), + }, + hir_nameres::Resolution::Builtin( + hir_nameres::BuiltinKind::Constructor(ctor), + ) => MonoExprKind::Con { + ctor: MonoId { + name: builtin_ctor_name(ctor).to_owned(), + ty: mono_ty, + span: expr.span, + }, + args: Vec::new(), + }, + hir_nameres::Resolution::ClassMethod { class, name } => { + let evidence = self + .class_method_value_evidence(expr_id, class) + .map(|evidence| self.subst.apply_evidence(self.driver.db, evidence)) + .or_else(|| { + self.driver.solve_class_method_pred( + class, + &name, + ty, + Some(expr.span), + ) + }); + if let Some(specialized) = evidence.and_then(|evidence| { + self.driver.resolve_class_method_call( + &name, evidence, ty, expr.span, self.depth, + ) + }) { + MonoExprKind::Var(MonoId { + name: specialized, + ty: mono_ty, + span: expr.span, + }) + } else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { + context: name, + }, + span: Some(expr.span), + }); + MonoExprKind::Error + } + } + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => { + let origin = self.driver.call_origin_for_def(def); + let name = if matches!(origin, MonoCallOrigin::Builtin(_)) { + def.name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))) + } else { + self.specialize_direct_function(def, mono_ty.ty(), expr.span) + }; + MonoExprKind::Var(MonoId { + name, + ty: mono_ty, + span: expr.span, + }) + } + _ => MonoExprKind::Field { + base: Box::new(self.expr(*base)?), + field: ident_text(self.driver.db, field), + }, + } + } else { + MonoExprKind::Field { + base: Box::new(self.expr(*base)?), + field: ident_text(self.driver.db, field), + } + } + } + ExprKind::BinOp { lhs, op, rhs } => self.bin_op_expr(BinOpExpr { + expr_id, + lhs: *lhs, + op: *op.atom(), + rhs: *rhs, + result_ty: ty, + span: expr.span, + })?, + ExprKind::UnaryOp { op, expr: operand } => { + self.un_op_expr(expr_id, *op.atom(), *operand, ty, expr.span)? + } + ExprKind::Index { base, index } => { + if self.is_storage_index_expr(*base) { + MonoExprKind::StorageIndex { + base: Box::new(self.expr(*base)?), + index: Box::new(self.expr(*index)?), + } + } else { + MonoExprKind::Index { + base: Box::new(self.expr(*base)?), + index: Box::new(self.expr(*index)?), + } + } + } + ExprKind::Proxy { ty, .. } => { + let ty = self.lower_body_ty(*ty)?; + let ty = self.subst.apply_ty(self.driver.db, ty); + MonoExprKind::Proxy(self.driver.mono_ty(ty, "proxy", expr.span)?) + } + ExprKind::Conversion { expr: inner, .. } => { + let Some(conversion) = checked_conversion else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: "checked conversion".to_owned(), + }, + span: Some(expr.span), + }); + return Some(MonoExpr { + span: expr.span, + ty: mono_ty, + kind: MonoExprKind::Error, + }); + }; + MonoExprKind::Conversion { + expr: Box::new(self.expr(*inner)?), + ty: mono_ty, + kind: conversion.kind, + } + } + ExprKind::TypeAscription { expr: inner, .. } => { + let mut inner = self.expr(*inner)?; + debug_assert_eq!(inner.ty, mono_ty); + inner.span = expr.span; + inner.ty = mono_ty; + self.lowered_exprs.insert(expr_id, inner.clone()); + return Some(inner); + } + ExprKind::If { + cond, + then_expr, + else_expr, + } => self.if_expr(expr_id, *cond, *then_expr, *else_expr, expr.span)?, + ExprKind::Lambda { params, body, .. } => { + self.lambda_expr(params.atom(), *body, ty, expr.span)? + } + ExprKind::DotCtor { name, args, .. } => MonoExprKind::Con { + ctor: MonoId { + name: match self.expr_resolution(expr_id) { + Some(hir_nameres::Resolution::Ctor { ty: adt, index }) => ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + Some(hir_nameres::Resolution::Builtin( + hir_nameres::BuiltinKind::Constructor(ctor), + )) => builtin_ctor_name(ctor).to_owned(), + _ => ident_text(self.driver.db, name), + }, + ty: mono_ty, + span: expr.span, + }, + args: args + .iter() + .map(|arg| self.expr(*arg)) + .collect::>>()?, + }, + ExprKind::Error => MonoExprKind::Error, + }; + let mono_expr = MonoExpr { + span: expr.span, + ty: mono_ty, + kind, + }; + self.lowered_exprs.insert(expr_id, mono_expr.clone()); + Some(mono_expr) + } + fn ident_expr( + &mut self, + expr_id: Id>, + name: &SpannedElem<'db, Ident<'db>>, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> MonoExprKind<'db> { + match self.expr_resolution(expr_id) { + Some(hir_nameres::Resolution::Ctor { ty: adt, index }) => MonoExprKind::Con { + ctor: MonoId { + name: ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + ty, + span, + }, + args: Vec::new(), + }, + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor(ctor))) => { + MonoExprKind::Con { + ctor: MonoId { + name: builtin_ctor_name(ctor).to_owned(), + ty, + span, + }, + args: Vec::new(), + } + } + Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + }) => { + let origin = self.driver.call_origin_for_def(def); + let name = if matches!(origin, MonoCallOrigin::Builtin(_)) { + def.name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))) + } else { + self.specialize_direct_function(def, ty.ty(), span) + }; + MonoExprKind::Var(MonoId { name, ty, span }) + } + _ => MonoExprKind::Var(MonoId { + name: ident_text(self.driver.db, name), + ty, + span, + }), + } + } + + fn lambda_expr( + &mut self, + params: &[FuncParam<'db>], + body: FuncBody<'db>, + ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let name = body + .def_id(self.driver.db) + .name(self.driver.db) + .unwrap_or_else(|| "lambda".to_owned()); + let TyKind::Function { + params: param_tys, .. + } = ty.kind(self.driver.db) + else { + return Some(MonoExprKind::Lambda { + name, + params: Vec::new(), + body: Vec::new(), + }); + }; + if params.len() != param_tys.len() { + return Some(MonoExprKind::Lambda { + name, + params: Vec::new(), + body: Vec::new(), + }); + } + + let mut locals = self.locals.clone(); + let mut mono_params = Vec::new(); + for (param, param_ty) in params.iter().zip(param_tys) { + let param_ty = self.subst.apply_ty(self.driver.db, *param_ty); + let name = param_name(self.driver.db, param).unwrap_or("_").to_owned(); + let mono_ty = self.driver.mono_ty(param_ty, "lambda parameter", span)?; + locals.insert(name.clone(), param_ty); + mono_params.push(MonoParam { + name, + mode: ParamMode::from_bool( + param_comptime(param) || ty_is_comptime(self.driver.db, param_ty), + ), + ty: mono_ty, + span: param.span(self.driver.db), + }); + } + + let body_map = self + .driver + .body_resolution_for(body) + .cloned() + .unwrap_or_else(|| self.body_map.clone()); + let result = self.result.clone(); + let subst = self.subst.clone(); + let info = self.info; + let depth = self.depth; + let mut nested = BodyCtx { + driver: self.driver, + info, + body, + result, + body_map, + pre_typeck_desugar: self.pre_typeck_desugar.clone(), + subst, + depth, + index: Arc::clone(&self.index), + lowered_exprs: FxHashMap::default(), + locals, + }; + let lowered_body = body + .top_level_stmts(nested.driver.db) + .iter() + .map(|stmt| nested.stmt(*stmt)) + .collect::>>()?; + + Some(MonoExprKind::Lambda { + name, + params: mono_params, + body: lowered_body, + }) + } + fn pat(&mut self, pat_id: Id>) -> Option> { + let pat = self.body.pats(self.driver.db).get(pat_id); + let ty = self + .index + .pat_tys + .get(&(self.body, pat_id)) + .copied() + .map(|ty| self.subst.apply_ty(self.driver.db, ty)) + .unwrap_or_else(|| Ty::unknown(self.driver.db)); + let mono_ty = self.driver.mono_ty(ty, "pattern", pat.span)?; + if let Some(kind) = self.bool_pat_kind(pat_id, mono_ty, pat.span) { + return Some(MonoPat { + span: pat.span, + ty: mono_ty, + kind, + }); + } + let kind = match &pat.kind { + PatKind::Wildcard => MonoPatKind::Wildcard, + PatKind::Var(name) => match self.pat_resolution(pat_id) { + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + ctor, + ))) => MonoPatKind::Con { + ctor: MonoId { + name: builtin_ctor_name(ctor).to_owned(), + ty: mono_ty, + span: pat.span, + }, + args: Vec::new(), + }, + // Same-name constructors lower as nullary constructor + // patterns, not binders. + Some(hir_nameres::Resolution::Ctor { ty: adt, index }) => MonoPatKind::Con { + ctor: MonoId { + name: ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + ty: mono_ty, + span: pat.span, + }, + args: Vec::new(), + }, + _ => MonoPatKind::Var(MonoId { + name: { + let name = ident_text(self.driver.db, name); + self.locals.insert(name.clone(), ty); + name + }, + ty: mono_ty, + span: pat.span, + }), + }, + PatKind::Lit(lit) => MonoPatKind::Lit(lit.clone()), + PatKind::Ctor { head, args } => MonoPatKind::Con { + ctor: MonoId { + name: match self.pat_resolution(pat_id) { + Some(hir_nameres::Resolution::Ctor { ty: adt, index }) => ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + Some(hir_nameres::Resolution::Builtin( + hir_nameres::BuiltinKind::Constructor(ctor), + )) => builtin_ctor_name(ctor).to_owned(), + _ => ident_text(self.driver.db, head.name()), + }, + ty: mono_ty, + span: pat.span, + }, + args: args + .iter() + .map(|arg| self.pat(*arg)) + .collect::>>()?, + }, + PatKind::Tuple { elems } => self.tuple_pat(pat_id, elems, ty, pat.span)?.kind, + PatKind::ComptimeLabel { expr, .. } => MonoPatKind::ComptimeLabel(self.expr(*expr)?), + PatKind::Error => MonoPatKind::Error, + }; + Some(MonoPat { + span: pat.span, + ty: mono_ty, + kind, + }) + } + + pub(super) fn expr_ty(&self, expr: Id>) -> Option> { + self.index.expr_tys.get(&(self.body, expr)).copied() + } + + fn adt_field_index(&self, expr: Id>) -> Option { + self.index + .adt_field_indices + .get(&(self.body, expr)) + .copied() + } + + fn checked_conversion(&self, expr: Id>) -> Option<&CheckedConversion<'db>> { + self.index.checked_conversions.get(&(self.body, expr)) + } + + fn function_value_ty(&mut self, def: DefId<'db>) -> Option> { + let info = self.driver.functions.get(&def).cloned()?; + let lowered = self.driver.try_lower_normalized_function(&info)?; + Some(Ty::function( + self.driver.db, + lowered.params.clone(), + lowered.ret, + )) + } + + fn close_method_constructor_ty( + &mut self, + ty: Ty<'db>, + callee: Id>, + args: &[Id>], + ) -> Option> { + let Some(hir_nameres::Resolution::Ctor { ty: adt, .. }) = self.expr_resolution(callee) + else { + return None; + }; + if adt.name(self.driver.db).as_deref() != Some("Method") { + return None; + } + let function_arg = args.get(4).copied()?; + let Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + }) = self.expr_resolution(function_arg) + else { + return None; + }; + let fn_ty = self.function_value_ty(def)?; + let TyKind::Named { ctor, args } = ty.kind(self.driver.db) else { + return None; + }; + let mut ty_args = args.clone(); + let fn_slot = ty_args.get_mut(4)?; + if ty_is_closed(self.driver.db, *fn_slot) { + return None; + } + *fn_slot = fn_ty; + Some(Ty::named(self.driver.db, *ctor, ty_args)) + } + + fn pat_resolution(&self, pat: Id>) -> Option> { + self.index.pat_resolutions.get(&(self.body, pat)).cloned() + } + + fn desugar_view(&self) -> BodyDesugarView<'_, 'db> { + BodyDesugarView::new(&self.pre_typeck_desugar) + } + + fn bool_expr_kind( + &self, + expr: Id>, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + let view = self.desugar_view().bool_expr_unit_sum(self.body, expr)?; + let resolved_value = match self.expr_resolution(expr) { + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::True, + ))) => true, + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::False, + ))) => false, + _ => return None, + }; + debug_assert_eq!(view.value, resolved_value); + Some(MonoExprKind::Con { + ctor: MonoId { + name: bool_ctor_name(resolved_value).to_owned(), + ty, + span, + }, + args: Vec::new(), + }) + } + + fn bool_pat_kind( + &self, + pat: Id>, + ty: MonoTy<'db>, + span: Span<'db>, + ) -> Option> { + let view = self.desugar_view().bool_pat_unit_sum(self.body, pat)?; + let resolved_value = match self.pat_resolution(pat) { + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::True, + ))) => true, + Some(hir_nameres::Resolution::Builtin(hir_nameres::BuiltinKind::Constructor( + hir_nameres::BuiltinCtor::False, + ))) => false, + _ => return None, + }; + debug_assert_eq!(view.value, resolved_value); + Some(self.bool_ctor_pat(resolved_value, ty, span).kind) + } + + fn if_stmt( + &mut self, + stmt_id: Id>, + fallback_cond: Id>, + fallback_then_body: &[Id>], + fallback_else_body: Option<&[Id>]>, + span: Span<'db>, + ) -> Option> { + let planned = self + .desugar_view() + .if_stmt_match(self.body, stmt_id) + .map(|view| { + ( + view.cond, + view.then_body.to_vec(), + view.else_body.map(|body| body.to_vec()), + ) + }); + let Some((cond, then_body, else_body)) = planned else { + return Some(MonoStmtKind::If { + cond: self.expr(fallback_cond)?, + then_body: self.stmts(fallback_then_body)?, + else_body: match fallback_else_body { + Some(body) => Some(self.stmts(body)?), + None => None, + }, + }); + }; + + let cond = self.expr(cond)?; + let bool_ty = cond.ty; + let then_body = self.stmts(&then_body)?; + let else_body = match else_body.as_deref() { + Some(body) => self.stmts(body)?, + None => Vec::new(), + }; + + Some(MonoStmtKind::Match { + scrutinees: vec![cond], + arms: vec![ + MonoArm { + span, + pats: vec![self.bool_ctor_pat(true, bool_ty, span)], + body: then_body, + }, + MonoArm { + span, + pats: vec![self.bool_ctor_pat(false, bool_ty, span)], + body: else_body, + }, + ], + }) + } + + fn if_expr( + &mut self, + expr_id: Id>, + fallback_cond: Id>, + fallback_then_expr: Id>, + fallback_else_expr: Id>, + span: Span<'db>, + ) -> Option> { + let planned = self + .desugar_view() + .if_expr_match(self.body, expr_id) + .map(|view| (view.cond, view.then_expr, view.else_expr)); + let (cond, then_expr, else_expr) = + planned.unwrap_or((fallback_cond, fallback_then_expr, fallback_else_expr)); + let cond = self.expr(cond)?; + let bool_ty = cond.ty; + Some(MonoExprKind::Match { + scrutinee: Box::new(cond), + arms: vec![ + MonoExprArm { + span, + pat: self.bool_ctor_pat(true, bool_ty, span), + expr: self.expr(then_expr)?, + }, + MonoExprArm { + span, + pat: self.bool_ctor_pat(false, bool_ty, span), + expr: self.expr(else_expr)?, + }, + ], + }) + } + + fn stmts(&mut self, stmts: &[Id>]) -> Option>> { + stmts.iter().map(|stmt| self.stmt(*stmt)).collect() + } + + fn bool_ctor_pat(&self, value: bool, ty: MonoTy<'db>, span: Span<'db>) -> MonoPat<'db> { + MonoPat { + span, + ty, + kind: MonoPatKind::Con { + ctor: MonoId { + name: bool_ctor_name(value).to_owned(), + ty, + span, + }, + args: Vec::new(), + }, + } + } + + fn tuple_expr_product_shape( + &self, + expr: Id>, + elems: &[Id>], + ) -> ProductShape>> { + self.desugar_view() + .tuple_expr_product(self.body, expr) + .cloned() + .unwrap_or_else(|| ProductShape::from_slice(elems)) + } + + fn tuple_pat_product_shape( + &self, + pat: Id>, + elems: &[Id>], + ) -> ProductShape>> { + self.desugar_view() + .tuple_pat_product(self.body, pat) + .cloned() + .unwrap_or_else(|| ProductShape::from_slice(elems)) + } + + fn tuple_expr( + &mut self, + expr: Id>, + elems: &[Id>], + ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let product = self.tuple_expr_product_shape(expr, elems); + let elems = product + .to_vec() + .iter() + .map(|elem| self.expr(*elem)) + .collect::>>()?; + Some(product_expr_from_elems(self.driver.db, &elems, ty, span)) + } + + fn tuple_pat( + &mut self, + pat: Id>, + elems: &[Id>], + ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let product = self.tuple_pat_product_shape(pat, elems); + let elems = product + .to_vec() + .iter() + .map(|elem| self.pat(*elem)) + .collect::>>()?; + Some(product_pat_from_elems(self.driver.db, &elems, ty, span)) + } + + fn is_storage_index_expr(&self, expr: Id>) -> bool { + if matches!( + self.expr_resolution(expr), + Some(hir_nameres::Resolution::Field(_)) + ) { + return true; + } + match &self.body.exprs(self.driver.db).get(expr).kind { + ExprKind::Index { base, .. } => self.is_storage_index_expr(*base), + ExprKind::TypeAscription { expr, .. } => self.is_storage_index_expr(*expr), + _ => false, + } + } + + pub(super) fn expr_resolution( + &self, + expr: Id>, + ) -> Option> { + self.index.expr_resolutions.get(&(self.body, expr)).cloned() + } + + fn constructor_call_result_ty(&self, callee: Id>) -> Option> { + if let Some(adt) = self.adt_for_ident_callee(callee) { + return Some(Ty::named( + self.driver.db, + TyCtor::User(UserTyCtor { + def: adt, + kind: UserTyCtorKind::Adt, + }), + Vec::new(), + )); + } + match self.expr_resolution(callee)? { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Adt, + } + | hir_nameres::Resolution::Ctor { ty: def, .. } => Some(Ty::named( + self.driver.db, + TyCtor::User(UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + Vec::new(), + )), + _ => None, + } + } + + pub(super) fn adt_for_ident_callee(&self, callee: Id>) -> Option> { + let ExprKind::Ident(name) = &self.body.exprs(self.driver.db).get(callee).kind else { + return None; + }; + let text = ident_text(self.driver.db, name); + self.driver + .adts + .keys() + .copied() + .find(|def| def.name(self.driver.db).as_deref() == Some(text.as_str())) + } + + pub(super) fn call_evidence( + &self, + call_expr: Id>, + callee_expr: Id>, + ) -> Option> { + self.index + .call_evidence + .get(&(self.body, call_expr, callee_expr)) + .cloned() + } + + fn class_method_value_evidence( + &self, + expr: Id>, + class: DefId<'db>, + ) -> Option> { + self.index + .class_method_value_evidence + .get(&(self.body, expr, class)) + .cloned() + } + + pub(super) fn invokable_call_main_ty( + &self, + call_expr: Id>, + callee_expr: Id>, + ) -> Option> { + let evidence = self.call_evidence(call_expr, callee_expr)?; + let obligation = self.result.obligations.get(evidence.obligation)?; + let PredKind::InClass { + class: ClassId::Builtin(BuiltinClassId::Invokable), + main, + .. + } = obligation.pred.kind(self.driver.db) + else { + return None; + }; + Some(self.subst.apply_ty(self.driver.db, *main)) + } + + fn invokable_call_result_ty( + &self, + call_expr: Id>, + callee_expr: Id>, + ) -> Option> { + let evidence = self.call_evidence(call_expr, callee_expr)?; + let obligation = self.result.obligations.get(evidence.obligation)?; + let PredKind::InClass { + class: ClassId::Builtin(BuiltinClassId::Invokable), + args, + .. + } = obligation.pred.kind(self.driver.db) + else { + return None; + }; + let ret = args.get(1).copied()?; + Some(self.subst.apply_ty(self.driver.db, ret)) + } + + pub(super) fn call_evidence_for_builtin_int( + &self, + span: Span<'db>, + ) -> Option> { + let _ = span; + self.index.first_builtin_int_evidence.clone() + } + + pub(super) fn is_int_from_integer_call(&self, callee: Id>) -> bool { + matches!( + self.expr_resolution(callee), + Some(hir_nameres::Resolution::Builtin( + hir_nameres::BuiltinKind::ClassMethod( + hir_nameres::BuiltinClassMethod::IntFromInteger + ) + )) + ) + } + + fn lower_body_ty(&mut self, ty: hir::ast::ty::TypeRef<'db>) -> Option> { + let lowerer = TypeLowering::from_body_resolutions( + self.driver.db, + &self.body_map, + BinderEnv::from_type_vars(&self.info.type_vars), + ); + let Some(resolution) = self.driver.try_module_resolution(self.info.module) else { + self.driver + .push_missing_module_resolution(Some(ty.span(self.driver.db))); + return None; + }; + let mut normalizer = AliasNormalizer::new( + self.driver.db, + self.info.module, + &resolution.item_resolutions, + ); + Some(normalizer.normalize_ty(lowerer.lower_type(ty))) + } + + fn stmt_has_comptime_let_obligation(&self, stmt: Id>) -> bool { + self.index.comptime_let_stmts.contains(&(self.body, stmt)) + } + + pub(super) fn comptime_obligations(&mut self) -> Option>> { + let obligations = self + .index + .comptime_obligations + .get(&self.body) + .cloned() + .unwrap_or_default(); + let mut out = Vec::new(); + for obligation in obligations { + let expr = match self.lowered_exprs.get(&obligation.expr).cloned() { + Some(expr) => expr, + None => self.expr(obligation.expr)?, + }; + let kind = match obligation.kind { + ComptimeObligationKind::LetInit { name, .. } => { + MonoComptimeObligationKind::LetInit { name } + } + ComptimeObligationKind::Return { context } => { + MonoComptimeObligationKind::Return { context } + } + ComptimeObligationKind::CallParam { + function, param, .. + } => MonoComptimeObligationKind::CallParam { function, param }, + ComptimeObligationKind::PatternLabel { .. } => { + MonoComptimeObligationKind::PatternLabel + } + }; + out.push(MonoComptimeObligation { + span: expr.span, + expr, + kind, + }); + } + Some(out) + } +} + +fn bool_ctor_name(value: bool) -> &'static str { + if value { + MonoBuiltinCtor::True.name() + } else { + MonoBuiltinCtor::False.name() + } +} diff --git a/crates/specialize/src/specialize/call_resolver.rs b/crates/specialize/src/specialize/call_resolver.rs new file mode 100644 index 00000000..72177f69 --- /dev/null +++ b/crates/specialize/src/specialize/call_resolver.rs @@ -0,0 +1,765 @@ +use super::*; + +impl<'a, 'db> BodyCtx<'a, 'db> { + pub(super) fn bin_op_expr(&mut self, expr: BinOpExpr<'db>) -> Option> { + match expr.op { + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Div + | BinOp::Mod + | BinOp::BitAnd + | BinOp::BitXor + | BinOp::BitOr + | BinOp::Eq + | BinOp::Gt + | BinOp::Pow => self.overloaded_bin_op_expr(expr), + BinOp::Shl | BinOp::Shr | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::GtEq => { + self.operator_function_bin_op_expr(expr) + } + // Preserve logical operators in monomorphic IR. Hull lowers these + // nodes to conditionals, which keeps the RHS short-circuited. + BinOp::And | BinOp::Or => Some(MonoExprKind::BinOp { + lhs: Box::new(self.expr(expr.lhs)?), + op: expr.op, + rhs: Box::new(self.expr(expr.rhs)?), + }), + _ => Some(MonoExprKind::BinOp { + lhs: Box::new(self.expr(expr.lhs)?), + op: expr.op, + rhs: Box::new(self.expr(expr.rhs)?), + }), + } + } + + fn overloaded_bin_op_expr(&mut self, expr: BinOpExpr<'db>) -> Option> { + let lhs_expr = self.expr(expr.lhs)?; + let rhs_expr = self.expr(expr.rhs)?; + let (class_name, method) = overloaded_operator_method(expr.op)?; + let callee_ty = Ty::function( + self.driver.db, + vec![lhs_expr.ty.ty(), rhs_expr.ty.ty()], + expr.result_ty, + ); + let mono_callee_ty = self + .driver + .mono_ty(callee_ty, "operator callee", expr.span)?; + let evidence = self + .call_evidence(expr.expr_id, expr.expr_id) + .map(|evidence| self.subst.apply_evidence(self.driver.db, evidence.evidence)) + .or_else(|| { + self.driver.solve_operator_method_pred( + class_name, + method, + callee_ty, + Some(expr.span), + ) + }); + let Some(evidence) = evidence else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { + context: method.to_owned(), + }, + span: Some(expr.span), + }); + return Some(MonoExprKind::BinOp { + lhs: Box::new(lhs_expr), + op: expr.op, + rhs: Box::new(rhs_expr), + }); + }; + + let Some(name) = self + .driver + .resolve_class_method_call(method, evidence, callee_ty, expr.span, self.depth) + else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { + context: method.to_owned(), + }, + span: Some(expr.span), + }); + return Some(MonoExprKind::BinOp { + lhs: Box::new(lhs_expr), + op: expr.op, + rhs: Box::new(rhs_expr), + }); + }; + + Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span: expr.span, + }, + origin: MonoCallOrigin::ByName, + args: vec![lhs_expr, rhs_expr], + }) + } + + pub(super) fn un_op_expr( + &mut self, + _expr_id: Id>, + op: UnOp, + operand: Id>, + result_ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let operand = self.expr(operand)?; + let fallback = || MonoExprKind::UnaryOp { + op, + expr: Box::new(operand.clone()), + }; + let UnOp::Not = op else { + return Some(fallback()); + }; + let callee_ty = Ty::function(self.driver.db, vec![operand.ty.ty()], result_ty); + let mono_callee_ty = self.driver.mono_ty(callee_ty, "operator callee", span)?; + let Some(resolution) = self.lookup_operator_function("not") else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: "operator not".to_owned(), + }, + span: Some(span), + }); + return Some(fallback()); + }; + + let (name, origin) = match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => { + let origin = self.driver.call_origin_for_def(def); + let name = if matches!(origin, MonoCallOrigin::Builtin(_)) { + def.name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))) + } else { + self.specialize_direct_function(def, callee_ty, span) + }; + (name, origin) + } + hir_nameres::Resolution::Builtin(kind) => ( + builtin_name(kind).to_owned(), + builtin_intrinsic(kind) + .map(MonoCallOrigin::Builtin) + .unwrap_or(MonoCallOrigin::ByName), + ), + _ => { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: "operator not".to_owned(), + }, + span: Some(span), + }); + return Some(fallback()); + } + }; + + Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin, + args: vec![operand], + }) + } + + fn operator_function_bin_op_expr(&mut self, expr: BinOpExpr<'db>) -> Option> { + let lhs_expr = self.expr(expr.lhs)?; + let rhs_expr = self.expr(expr.rhs)?; + let name = plain_operator_function(expr.op)?; + let fallback_lhs = lhs_expr.clone(); + let fallback_rhs = rhs_expr.clone(); + let (first_expr, second_expr) = if matches!(expr.op, BinOp::Shl | BinOp::Shr) { + (rhs_expr, lhs_expr) + } else { + (lhs_expr, rhs_expr) + }; + let callee_ty = Ty::function( + self.driver.db, + vec![first_expr.ty.ty(), second_expr.ty.ty()], + expr.result_ty, + ); + let mono_callee_ty = self + .driver + .mono_ty(callee_ty, "operator callee", expr.span)?; + let Some(resolution) = self.lookup_operator_function(name) else { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: format!("operator {name}"), + }, + span: Some(expr.span), + }); + return Some(MonoExprKind::BinOp { + lhs: Box::new(fallback_lhs), + op: expr.op, + rhs: Box::new(fallback_rhs), + }); + }; + + match resolution { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + } => { + let origin = self.driver.call_origin_for_def(def); + let callee_name = if matches!(origin, MonoCallOrigin::Builtin(_)) { + def.name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))) + } else { + self.specialize_direct_function(def, callee_ty, expr.span) + }; + Some(MonoExprKind::Call { + callee: MonoId { + name: callee_name, + ty: mono_callee_ty, + span: expr.span, + }, + origin, + args: vec![first_expr, second_expr], + }) + } + hir_nameres::Resolution::Builtin(kind) => { + let origin = builtin_intrinsic(kind) + .map(MonoCallOrigin::Builtin) + .unwrap_or(MonoCallOrigin::ByName); + Some(MonoExprKind::Call { + callee: MonoId { + name: builtin_name(kind).to_owned(), + ty: mono_callee_ty, + span: expr.span, + }, + origin, + args: vec![first_expr, second_expr], + }) + } + _ => { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: format!("operator {name}"), + }, + span: Some(expr.span), + }); + Some(MonoExprKind::BinOp { + lhs: Box::new(fallback_lhs), + op: expr.op, + rhs: Box::new(fallback_rhs), + }) + } + } + } + + fn lookup_operator_function(&self, name: &str) -> Option> { + let file = self + .info + .module + .def_id_value(self.driver.db) + .file(self.driver.db); + if let Some(module_id) = module_id_for_source_file(self.driver.db, file) { + let env = nameres::module_env(self.driver.db, module_id); + let local = env + .item_scope + .as_ref() + .and_then(|scope| scope.term_resolution(name)); + return local.or_else(|| env.terms.get(name).cloned()); + } + + hir_nameres::item_scope(self.driver.db, self.info.module).term_resolution(name) + } + pub(super) fn call_expr( + &mut self, + call_expr: Id>, + callee: Id>, + args: &[Id>], + result_ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let arg_exprs = args + .iter() + .map(|arg| self.expr(*arg)) + .collect::>>()?; + let resolution = self.expr_resolution(callee); + let mut callee_ty = self + .expr_ty(callee) + .map(|ty| self.subst.apply_ty(self.driver.db, ty)) + .unwrap_or_else(|| Ty::unknown(self.driver.db)); + if !ty_is_closed(self.driver.db, callee_ty) + && matches!( + resolution, + Some(hir_nameres::Resolution::Def { + kind: hir_nameres::DefResolutionKind::Function, + .. + }) + ) + { + callee_ty = Ty::function( + self.driver.db, + arg_exprs.iter().map(|arg| arg.ty.ty()).collect(), + result_ty, + ); + } else if !ty_is_closed(self.driver.db, callee_ty) + && let Some(invokable_ty) = self.invokable_call_main_ty(call_expr, callee) + { + callee_ty = invokable_ty; + } + if !matches!(callee_ty.kind(self.driver.db), TyKind::Function { .. }) { + callee_ty = Ty::function( + self.driver.db, + arg_exprs.iter().map(|arg| arg.ty.ty()).collect(), + result_ty, + ); + } + let mono_callee_ty = self.driver.mono_ty(callee_ty, "callee", span)?; + match resolution { + Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Function, + }) => { + let origin = self.driver.call_origin_for_def(def); + let name = if matches!(origin, MonoCallOrigin::Builtin(_)) { + def.name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))) + } else { + self.specialize_direct_function(def, callee_ty, span) + }; + Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin, + args: arg_exprs, + }) + } + Some(hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Adt, + }) => Some(MonoExprKind::Con { + ctor: MonoId { + name: def + .name(self.driver.db) + .unwrap_or_else(|| "ctor".to_owned()), + ty: self.driver.mono_ty(result_ty, "constructor", span)?, + span, + }, + args: arg_exprs, + }), + Some(hir_nameres::Resolution::Ctor { ty: adt, index }) => Some(MonoExprKind::Con { + ctor: MonoId { + name: ctor_name( + self.driver.db, + self.driver.adts.get(&adt).map(|info| info.adt), + index, + ), + ty: mono_callee_ty, + span, + }, + args: arg_exprs, + }), + Some(hir_nameres::Resolution::ClassMethod { class, name }) => { + if self.is_int_from_integer_call(callee) { + return self.int_from_integer_call(arg_exprs, result_ty, span); + } + let evidence = self + .call_evidence(call_expr, callee) + .map(|evidence| self.subst.apply_evidence(self.driver.db, evidence.evidence)) + .or_else(|| { + self.driver + .solve_class_method_pred(class, &name, callee_ty, Some(span)) + }); + if let Some(evidence) = evidence + && let Some(name) = self + .driver + .resolve_class_method_call(&name, evidence, callee_ty, span, self.depth) + { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin: MonoCallOrigin::ByName, + args: arg_exprs, + }); + } + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { context: name }, + span: Some(span), + }); + Some(MonoExprKind::ClosureDispatch { + callee: Box::new(self.expr(callee)?), + args: arg_exprs, + }) + } + Some(hir_nameres::Resolution::Builtin(kind)) => { + if matches!( + kind, + hir_nameres::BuiltinKind::ClassMethod( + hir_nameres::BuiltinClassMethod::IntFromInteger + ) + ) { + return self.int_from_integer_call(arg_exprs, result_ty, span); + } + let builtin_callee = MonoId { + name: builtin_name(kind).to_owned(), + ty: mono_callee_ty, + span, + }; + let origin = builtin_intrinsic(kind) + .map(MonoCallOrigin::Builtin) + .unwrap_or(MonoCallOrigin::ByName); + match kind { + hir_nameres::BuiltinKind::Constructor(_) => Some(MonoExprKind::Con { + ctor: builtin_callee, + args: arg_exprs, + }), + hir_nameres::BuiltinKind::ClassMethod( + hir_nameres::BuiltinClassMethod::InvokableInvoke, + ) => { + let evidence = self.call_evidence(call_expr, callee).map(|evidence| { + self.subst.apply_evidence(self.driver.db, evidence.evidence) + }); + if let Some(evidence) = evidence + && let Some(name) = self.driver.resolve_class_method_call( + "invoke", evidence, callee_ty, span, self.depth, + ) + { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin: MonoCallOrigin::ByName, + args: arg_exprs, + }); + } + self.invokable_closure_dispatch(arg_exprs, span) + } + _ => Some(MonoExprKind::Call { + callee: builtin_callee, + origin, + args: arg_exprs, + }), + } + } + _ => { + if let Some(adt) = self.adt_for_ident_callee(callee) { + return Some(MonoExprKind::Con { + ctor: MonoId { + name: adt + .name(self.driver.db) + .unwrap_or_else(|| "ctor".to_owned()), + ty: self.driver.mono_ty(result_ty, "constructor", span)?, + span, + }, + args: arg_exprs, + }); + } + if let Some((class, name)) = self.qualified_class_method(callee) { + let evidence = self + .call_evidence(call_expr, callee) + .map(|evidence| { + self.subst.apply_evidence(self.driver.db, evidence.evidence) + }) + .or_else(|| { + self.driver + .solve_class_method_pred(class, &name, callee_ty, Some(span)) + }); + if let Some(evidence) = evidence + && let Some(name) = self + .driver + .resolve_class_method_call(&name, evidence, callee_ty, span, self.depth) + { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin: MonoCallOrigin::ByName, + args: arg_exprs, + }); + } + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { context: name }, + span: Some(span), + }); + return Some(MonoExprKind::ClosureDispatch { + callee: Box::new(self.expr(callee)?), + args: arg_exprs, + }); + } + if let Some((name, intrinsic)) = self.qualified_std_intrinsic(callee) { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin: MonoCallOrigin::Builtin(intrinsic), + args: arg_exprs, + }); + } + if let Some((name, intrinsic)) = self.unqualified_std_intrinsic(callee) { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: mono_callee_ty, + span, + }, + origin: MonoCallOrigin::Builtin(intrinsic), + args: arg_exprs, + }); + } + Some(MonoExprKind::ClosureDispatch { + callee: Box::new(self.expr(callee)?), + args: arg_exprs, + }) + } + } + } + + fn qualified_class_method(&self, callee: Id>) -> Option<(DefId<'db>, String)> { + let ExprKind::Field { base, field } = &self.body.exprs(self.driver.db).get(callee).kind + else { + return None; + }; + match self.expr_resolution(*base)? { + hir_nameres::Resolution::Def { + def, + kind: hir_nameres::DefResolutionKind::Class, + } => Some((def, ident_text(self.driver.db, field))), + hir_nameres::Resolution::Err => { + let ExprKind::Ident(name) = &self.body.exprs(self.driver.db).get(*base).kind else { + return None; + }; + let name = ident_text(self.driver.db, name); + self.driver + .unique_class_named(&name) + .map(|def| (def, ident_text(self.driver.db, field))) + } + _ => None, + } + } + + fn qualified_std_intrinsic(&self, callee: Id>) -> Option<(String, MonoIntrinsic)> { + let ExprKind::Field { base, field } = &self.body.exprs(self.driver.db).get(callee).kind + else { + return None; + }; + let Some(hir_nameres::Resolution::Module(module_ref)) = self.expr_resolution(*base) else { + return None; + }; + if module_ref.name != "std" { + return None; + } + let name = ident_text(self.driver.db, field); + self.driver + .std_intrinsic_named(&name) + .map(|intrinsic| (name, intrinsic)) + } + + fn unqualified_std_intrinsic(&self, callee: Id>) -> Option<(String, MonoIntrinsic)> { + let ExprKind::Ident(name) = &self.body.exprs(self.driver.db).get(callee).kind else { + return None; + }; + if !matches!( + self.expr_resolution(callee), + Some(hir_nameres::Resolution::Err) + ) { + return None; + } + let local_name = ident_text(self.driver.db, name); + let source_name = self.std_selected_import_name(&local_name)?; + self.driver + .std_intrinsic_named(&source_name) + .map(|intrinsic| (source_name, intrinsic)) + } + + fn std_selected_import_name(&self, local_name: &str) -> Option { + self.info + .module + .items(self.driver.db) + .iter() + .find_map(|item| match item { + Item::Import(import) => self.std_import_selected_name(*import, local_name), + _ => None, + }) + } + + fn std_import_selected_name(&self, import: Import<'db>, local_name: &str) -> Option { + let path = import.path_elems(self.driver.db); + if path.len() != 1 || ident_text(self.driver.db, &path[0]) != "std" { + return None; + } + match import.selector(self.driver.db).as_ref()? { + ImportSelector::Wildcard => { + let hidden = import + .hiding(self.driver.db) + .iter() + .any(|hidden| ident_text(self.driver.db, &hidden.name) == local_name); + (!hidden).then(|| local_name.to_owned()) + } + ImportSelector::Names(names) => names.iter().find_map(|selected| { + let source_name = ident_text(self.driver.db, &selected.name); + let selected_local = selected + .alias + .as_ref() + .map(|alias| ident_text(self.driver.db, alias)) + .unwrap_or_else(|| source_name.clone()); + (selected_local == local_name).then_some(source_name) + }), + } + } + + fn invokable_closure_dispatch( + &mut self, + mut arg_exprs: Vec>, + span: Span<'db>, + ) -> Option> { + if arg_exprs.is_empty() { + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingEvidence { + context: "invokable.invoke".to_owned(), + }, + span: Some(span), + }); + return Some(MonoExprKind::Error); + } + let callee = arg_exprs.remove(0); + Some(MonoExprKind::ClosureDispatch { + callee: Box::new(callee), + args: arg_exprs, + }) + } + + pub(super) fn specialize_direct_function( + &mut self, + def: DefId<'db>, + callee_ty: Ty<'db>, + span: Span<'db>, + ) -> String { + if !self + .driver + .ensure_specialization_type_size(&[callee_ty], Some(span)) + { + return def + .name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))); + } + if let Some(info) = self.driver.functions.get(&def).cloned() { + let base = self.driver.source_base_name(&info); + let Some(lowered) = self.driver.try_lower_normalized_function(&info) else { + return base; + }; + let mut subst = TySubst::default(); + subst.match_ty( + self.driver.db, + lowered.scheme.body(self.driver.db).ty(self.driver.db), + callee_ty, + ); + let givens = self.driver.function_givens(&info, &lowered); + self.driver + .resolve_mptc_from_preds(info.module, &givens, &mut subst); + let args = subst.specialization_args(); + if !self + .driver + .ensure_specialization_type_size(&args, Some(span)) + { + return base; + } + let name = specialize_name(self.driver.db, &base, &args); + let key = SpecKey { + def, + ty: callee_ty, + base_name: name, + origin: MonoFunctionOrigin::Source, + }; + return self.driver.enqueue(key, self.depth + 1); + } + let name = def + .name(self.driver.db) + .unwrap_or_else(|| format!("{:?}", def.kind(self.driver.db))); + self.driver.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::UnresolvedExternal { + function: def, + name: name.clone(), + }, + span: Some(span), + }); + name + } + + fn int_from_integer_call( + &mut self, + mut args: Vec>, + result_ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + if ty_is_builtin(self.driver.db, result_ty, BuiltinTyCtor::Integer) { + return Some( + args.pop() + .map(|expr| expr.kind) + .unwrap_or(MonoExprKind::Error), + ); + } + if ty_is_builtin(self.driver.db, result_ty, BuiltinTyCtor::Word) { + let ty = Ty::function( + self.driver.db, + vec![Ty::integer(self.driver.db)], + Ty::word(self.driver.db), + ); + return Some(MonoExprKind::Call { + callee: MonoId { + name: "wordFromInteger".to_owned(), + ty: MonoTy::new_unchecked(ty), + span, + }, + origin: MonoCallOrigin::Builtin(MonoIntrinsic::WordFromInteger), + args, + }); + } + if let Some(evidence) = self.call_evidence_for_builtin_int(span) { + let evidence = self.subst.apply_evidence(self.driver.db, evidence.evidence); + if let Some(name) = self.driver.resolve_class_method_call( + "fromInteger", + evidence, + Ty::function(self.driver.db, vec![Ty::integer(self.driver.db)], result_ty), + span, + self.depth, + ) { + return Some(MonoExprKind::Call { + callee: MonoId { + name, + ty: MonoTy::new_unchecked(Ty::function( + self.driver.db, + vec![Ty::integer(self.driver.db)], + result_ty, + )), + span, + }, + origin: MonoCallOrigin::ByName, + args, + }); + } + } + Some(MonoExprKind::Call { + callee: MonoId { + name: "Int_fromInteger".to_owned(), + ty: MonoTy::new_unchecked(Ty::function( + self.driver.db, + vec![Ty::integer(self.driver.db)], + result_ty, + )), + span, + }, + origin: MonoCallOrigin::ByName, + args, + }) + } +} diff --git a/crates/specialize/src/specialize/derived_generic.rs b/crates/specialize/src/specialize/derived_generic.rs new file mode 100644 index 00000000..8b066007 --- /dev/null +++ b/crates/specialize/src/specialize/derived_generic.rs @@ -0,0 +1,191 @@ +use super::*; + +impl<'db> Driver<'db> { + pub(super) fn specialize_derived_generic( + &mut self, + adt: DefId<'db>, + method: &str, + main: Ty<'db>, + rep: Ty<'db>, + target_ty: Ty<'db>, + span: Span<'db>, + ) -> Option { + let key = SyntheticKey { + adt, + method: method.to_owned(), + main, + rep, + }; + if let Some(name) = self.synthetic.get(&key) { + return Some(name.clone()); + } + if !self.ensure_specialization_type_size(&[main, rep, target_ty], Some(span)) { + return None; + } + let name = specialize_name( + self.db, + &format!("Generic_{method}_{}", def_hash_suffix(self.db, adt)), + &[main, rep], + ); + self.synthetic.insert(key.clone(), name.clone()); + self.synthetic_order.push(key.clone()); + let Some(fun) = self.build_derived_generic_function(&key, &name, target_ty, span) else { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::UnsupportedEvidence { + context: format!("cannot generate Generic.{method}"), + }, + span: Some(span), + }); + return Some(name); + }; + self.synthetic_funs.insert(key, fun); + Some(name) + } + + fn build_derived_generic_function( + &mut self, + key: &SyntheticKey<'db>, + name: &str, + _target_ty: Ty<'db>, + span: Span<'db>, + ) -> Option> { + let info = self.adts.get(&key.adt)?; + let adt = info.adt; + let plan = derived_generic_plan(self.db, info.module, adt)?; + let mut subst = TySubst::default(); + let adt_head = Ty::named( + self.db, + TyCtor::User(UserTyCtor { + def: key.adt, + kind: UserTyCtorKind::Adt, + }), + (0..adt.ty_param_elems(self.db).len()) + .map(|index| Ty::bound(self.db, index as u32)) + .collect(), + ); + subst.match_ty(self.db, adt_head, key.main); + let rep = subst.apply_ty(self.db, plan.rep); + let method = key.method.as_str(); + let (param_ty, ret_ty) = match method { + "from" => (key.main, rep), + "to" => (rep, key.main), + _ => return None, + }; + let param = MonoParam { + name: "x".to_owned(), + mode: ParamMode::Runtime, + ty: MonoTy::new_unchecked(param_ty), + span, + }; + let x_id = MonoId { + name: "x".to_owned(), + ty: MonoTy::new_unchecked(param_ty), + span, + }; + let x_expr = MonoExpr { + span, + ty: MonoTy::new_unchecked(param_ty), + kind: MonoExprKind::Var(x_id.clone()), + }; + let arms = if method == "from" { + plan.from_arms + .iter() + .map(|arm| { + let product_rep = subst.apply_ty(self.db, arm.product_rep); + let vars = + product_vars(self.db, product_rep, arm.field_count as usize, span, "f")?; + let pat = MonoPat { + span, + ty: MonoTy::new_unchecked(key.main), + kind: MonoPatKind::Con { + ctor: MonoId { + name: format!( + "{}_{}", + key.adt.name(self.db).unwrap_or_else(|| "Adt".to_owned()), + arm.ctor_name + ), + ty: MonoTy::new_unchecked(key.main), + span, + }, + args: vars.iter().map(|var| var_pattern(var, span)).collect(), + }, + }; + let payload = product_expr_from_vars(self.db, &vars, product_rep, span); + let expr = + wrap_sum_expr(self.db, payload, rep, arm.inr_depth, arm.wraps_inl, span); + Some(MonoArm { + span, + pats: vec![pat], + body: vec![MonoStmt { + span, + kind: MonoStmtKind::Return(Some(expr)), + }], + }) + }) + .collect::>>()? + } else { + plan.to_arms + .iter() + .map(|arm| { + let product_rep = subst.apply_ty(self.db, arm.product_rep); + let vars = + product_vars(self.db, product_rep, arm.field_count as usize, span, "f")?; + let payload_pat = product_pat_from_vars(self.db, &vars, product_rep, span); + let pat = unwrap_sum_pat( + self.db, + payload_pat, + rep, + arm.inr_depth, + arm.wraps_inl, + span, + ); + let ctor = MonoId { + name: format!( + "{}_{}", + key.adt.name(self.db).unwrap_or_else(|| "Adt".to_owned()), + arm.ctor_name + ), + ty: MonoTy::new_unchecked(key.main), + span, + }; + let expr = MonoExpr { + span, + ty: MonoTy::new_unchecked(key.main), + kind: MonoExprKind::Con { + ctor, + args: vars.iter().map(|var| var_expr(var, span)).collect(), + }, + }; + Some(MonoArm { + span, + pats: vec![pat], + body: vec![MonoStmt { + span, + kind: MonoStmtKind::Return(Some(expr)), + }], + }) + }) + .collect::>>()? + }; + Some(MonoFunction { + origin: MonoFunctionOrigin::DerivedGeneric { + adt: key.adt, + method: method.to_owned(), + }, + source: None, + shadowed_top_level: None, + name: name.to_owned(), + span, + params: vec![param], + ret: MonoTy::new_unchecked(ret_ty), + comptime_obligations: Vec::new(), + body: vec![MonoStmt { + span, + kind: MonoStmtKind::Match { + scrutinees: vec![x_expr], + arms, + }, + }], + }) + } +} diff --git a/crates/specialize/src/specialize/diagnostics.rs b/crates/specialize/src/specialize/diagnostics.rs new file mode 100644 index 00000000..7b458bda --- /dev/null +++ b/crates/specialize/src/specialize/diagnostics.rs @@ -0,0 +1,289 @@ +use super::*; + +/// Specializer diagnostic. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpecializeDiagnostic<'db> { + pub kind: SpecializeDiagnosticKind<'db>, + pub span: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SpecializeDiagnosticKind<'db> { + FreeTypeVariable { + context: String, + ty: String, + }, + InstantiationFuelExhausted { + limit: usize, + }, + InstantiationDepthExceeded { + limit: usize, + }, + TypeSizeExceeded { + limit: usize, + }, + MissingBody { + function: DefId<'db>, + }, + MissingResolution { + context: String, + }, + MissingEvidence { + context: String, + }, + UnsupportedEvidence { + context: String, + }, + UnresolvedExternal { + function: DefId<'db>, + name: String, + }, + ComptimeEvaluationFailed { + context: String, + }, + ComptimeFuelExhausted { + function: String, + limit: usize, + }, + ComptimeRecursion { + function: String, + }, + ReductionRecursion { + function: String, + shadowed_top_level: Option, + }, + ReductionFuelExhausted { + function: String, + limit: usize, + }, + IntegerErasure { + context: String, + ty: String, + }, + UnsupportedRuntimeType { + context: String, + ty: String, + }, + PublicComptimeParam { + function: String, + param: String, + }, +} + +impl<'db> SpecializeDiagnostic<'db> { + pub fn lower(&self, db: &'db dyn HirDb) -> Diagnostic { + let mut diagnostic = Diagnostic::error(self.kind.to_string()).with_code(self.kind.code()); + diagnostic = if let Some(span) = self.span { + diagnostic.with_primary_label(db, span, Some(self.kind.primary_label())) + } else { + diagnostic + }; + for note in self.kind.notes() { + diagnostic = diagnostic.with_note(note); + } + diagnostic + } +} + +impl SpecializeDiagnosticKind<'_> { + pub fn code(&self) -> &'static str { + match self { + Self::FreeTypeVariable { .. } => DiagnosticCode::SPECIALIZE_FREE_TYPE_VARIABLE, + Self::InstantiationFuelExhausted { .. } => { + DiagnosticCode::SPECIALIZE_INSTANTIATION_FUEL_EXHAUSTED + } + Self::InstantiationDepthExceeded { .. } => { + DiagnosticCode::SPECIALIZE_INSTANTIATION_DEPTH_EXCEEDED + } + Self::TypeSizeExceeded { .. } => DiagnosticCode::SPECIALIZE_TYPE_SIZE_EXCEEDED, + Self::MissingBody { .. } => DiagnosticCode::SPECIALIZE_MISSING_BODY, + Self::MissingResolution { .. } => DiagnosticCode::SPECIALIZE_MISSING_RESOLUTION, + Self::MissingEvidence { .. } => DiagnosticCode::SPECIALIZE_MISSING_EVIDENCE, + Self::UnsupportedEvidence { .. } => DiagnosticCode::SPECIALIZE_UNSUPPORTED_EVIDENCE, + Self::UnresolvedExternal { .. } => DiagnosticCode::SPECIALIZE_UNRESOLVED_EXTERNAL, + Self::ComptimeEvaluationFailed { .. } => { + DiagnosticCode::SPECIALIZE_COMPTIME_EVALUATION_FAILED + } + Self::ComptimeFuelExhausted { .. } => { + DiagnosticCode::SPECIALIZE_COMPTIME_FUEL_EXHAUSTED + } + Self::ComptimeRecursion { .. } => DiagnosticCode::SPECIALIZE_COMPTIME_FUEL_EXHAUSTED, + Self::ReductionRecursion { .. } => DiagnosticCode::SPECIALIZE_REDUCTION_RECURSION, + Self::ReductionFuelExhausted { .. } => { + DiagnosticCode::SPECIALIZE_REDUCTION_FUEL_EXHAUSTED + } + Self::IntegerErasure { .. } => DiagnosticCode::SPECIALIZE_INTEGER_ERASURE, + Self::UnsupportedRuntimeType { .. } => { + DiagnosticCode::SPECIALIZE_UNSUPPORTED_RUNTIME_TYPE + } + Self::PublicComptimeParam { .. } => DiagnosticCode::SPECIALIZE_PUBLIC_COMPTIME_PARAM, + } + } + + fn primary_label(&self) -> &'static str { + match self { + Self::FreeTypeVariable { .. } => "type must be concrete here", + Self::InstantiationFuelExhausted { .. } => "specialization limit reached here", + Self::InstantiationDepthExceeded { .. } => "specialization depth limit reached here", + Self::TypeSizeExceeded { .. } => "specialization type size limit reached here", + Self::MissingBody { .. } => "function body required here", + Self::MissingResolution { .. } => "name resolution required here", + Self::MissingEvidence { .. } => "trait evidence required here", + Self::UnsupportedEvidence { .. } => "unsupported trait evidence here", + Self::UnresolvedExternal { .. } => "external function required here", + Self::ComptimeEvaluationFailed { .. } => "comptime evaluation failed here", + Self::ComptimeFuelExhausted { .. } => "comptime fuel limit reached here", + Self::ComptimeRecursion { .. } => "recursive comptime call cannot be reduced here", + Self::ReductionRecursion { .. } => "recursive call cannot be reduced here", + Self::ReductionFuelExhausted { .. } => { + "compile-time reduction depth limit reached here" + } + Self::IntegerErasure { .. } | Self::UnsupportedRuntimeType { .. } => { + "not representable at runtime" + } + Self::PublicComptimeParam { .. } => "external ABI parameter is runtime", + } + } + + fn notes(&self) -> Vec { + match self { + Self::FreeTypeVariable { context, .. } if context == "entry specialization" => vec![ + "entry points are specialization roots and must have a single concrete type" + .to_owned(), + "help: give the entry point a monomorphic signature or call a polymorphic helper from a monomorphic wrapper" + .to_owned(), + ], + Self::FreeTypeVariable { .. } => vec![ + "this can happen when a constructor or expression leaves a type parameter unresolved" + .to_owned(), + "help: add a type annotation that fixes the concrete type".to_owned(), + ], + Self::InstantiationFuelExhausted { .. } => vec![ + "help: reduce the number of specializations or raise --pe-max-instantiations when using solcore-driver" + .to_owned(), + ], + Self::InstantiationDepthExceeded { .. } => vec![ + "help: simplify the specialization chain or raise --pe-depth when using solcore-driver" + .to_owned(), + ], + Self::TypeSizeExceeded { .. } => vec![ + "help: simplify the specialized type or raise --pe-max-type-nodes when using solcore-driver" + .to_owned(), + ], + Self::ComptimeFuelExhausted { .. } => vec![ + "comptime evaluation did not finish before the fuel limit was reached".to_owned(), + "help: make the comptime recursion reach a base case, reduce the compile-time work, or raise --pe-fuel/--pe-depth when using solcore-driver" + .to_owned(), + ], + Self::ComptimeRecursion { .. } => vec![ + "comptime evaluation did not terminate because recursive calls form a cycle" + .to_owned(), + "help: make the comptime recursion reach a base case or reduce the compile-time work" + .to_owned(), + ], + Self::ReductionRecursion { + shadowed_top_level, + .. + } => { + let mut notes = vec![ + "help: add a base case, or guard the recursive call behind a runtime condition so it compiles to a runtime call" + .to_owned(), + ]; + if let Some(name) = shadowed_top_level { + notes.push(format!( + "a top-level function `{name}` is shadowed here; qualify the call to reach it" + )); + } + notes + } + Self::ReductionFuelExhausted { .. } => vec![ + "pure-call reduction did not finish before the depth limit was reached".to_owned(), + "help: add a base case, guard recursion behind a runtime condition, reduce the compile-time work, or raise --pe-fuel/--pe-depth when using solcore-driver" + .to_owned(), + ], + Self::IntegerErasure { .. } => vec![ + "`integer` and `comptime` values must be eliminated before runtime lowering" + .to_owned(), + "help: evaluate the value at comptime or change it to a runtime-representable type" + .to_owned(), + ], + Self::UnsupportedRuntimeType { .. } => vec![ + "fixed-length arrays do not yet have a runtime representation".to_owned(), + "help: avoid fixed-length arrays until backend layout support is implemented" + .to_owned(), + ], + Self::PublicComptimeParam { .. } => vec![ + "externally visible function parameters are supplied from calldata at runtime" + .to_owned(), + "help: remove `comptime` from the ABI parameter or call a private comptime helper with a compile-time value" + .to_owned(), + ], + _ => Vec::new(), + } + } +} + +impl fmt::Display for SpecializeDiagnosticKind<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FreeTypeVariable { context, ty } => { + if context == "entry specialization" { + write!( + f, + "entry point must have a concrete, non-polymorphic type before specialization" + ) + } else if ty == "_" { + write!(f, "cannot specialize {context}: type is not concrete") + } else { + write!( + f, + "cannot specialize {context}: unresolved type parameter in {ty}" + ) + } + } + Self::InstantiationFuelExhausted { limit } => { + write!(f, "specialization fuel exhausted at {limit} instantiations") + } + Self::InstantiationDepthExceeded { limit } => { + write!(f, "specialization depth exceeded at {limit}") + } + Self::TypeSizeExceeded { limit } => { + write!(f, "specialization type size exceeded at {limit} type nodes") + } + Self::MissingBody { .. } => write!(f, "missing function body during specialization"), + Self::MissingResolution { context } => write!(f, "missing resolution: {context}"), + Self::MissingEvidence { context } => write!(f, "missing evidence: {context}"), + Self::UnsupportedEvidence { context } => write!(f, "unsupported evidence: {context}"), + Self::UnresolvedExternal { name, .. } => write!(f, "unresolved external: {name}"), + Self::ComptimeEvaluationFailed { context } => { + write!(f, "comptime evaluation failed: {context}") + } + Self::ComptimeFuelExhausted { function, limit } => write!( + f, + "comptime evaluation fuel exhausted in {function} at {limit} unfold steps" + ), + Self::ComptimeRecursion { function } => write!( + f, + "comptime evaluation did not terminate in {function}: recursive calls form a cycle" + ), + Self::ReductionRecursion { function, .. } => write!( + f, + "`{function}` cannot be reduced at compile time: recursive calls form a cycle with no base case (infinite recursion)" + ), + Self::ReductionFuelExhausted { function, limit } => write!( + f, + "compile-time reduction fuel exhausted in {function} at {limit} unfold steps" + ), + Self::IntegerErasure { context, ty } => { + write!(f, "runtime lowering cannot represent `{ty}` in {context}") + } + Self::UnsupportedRuntimeType { context, ty } => { + write!(f, "runtime lowering cannot represent `{ty}` in {context}") + } + Self::PublicComptimeParam { function, param } => write!( + f, + "externally visible function `{function}` cannot take comptime parameter `{param}`" + ), + } + } +} diff --git a/crates/specialize/src/specialize/driver.rs b/crates/specialize/src/specialize/driver.rs new file mode 100644 index 00000000..82ccb986 --- /dev/null +++ b/crates/specialize/src/specialize/driver.rs @@ -0,0 +1,1299 @@ +use super::*; + +pub(super) struct Driver<'db> { + pub(super) db: &'db dyn Db, + pub(super) prepared: PreparedModule<'db>, + pub(super) module: Module<'db>, + pub(super) entry_module: Option>, + pub(super) modules: Vec>, + pub(super) options: SpecializeOptions, + pub(super) module_resolutions: FxHashMap, hir_nameres::ModuleResolutionMap<'db>>, + pub(super) module_trait_envs: FxHashMap, hir_ty::TraitEnvId<'db>>, + pub(super) functions: FxHashMap, FunctionInfo<'db>>, + pub(super) body_maps: FxHashMap, hir_nameres::BodyResolutionMap<'db>>, + pub(super) classes: FxHashMap, ClassInfo<'db>>, + pub(super) instances: FxHashMap, InstanceInfo<'db>>, + pub(super) adts: FxHashMap, AdtInfo<'db>>, + pub(super) specs: FxHashMap, String>, + pub(super) spec_order: Vec>, + pub(super) mono_funs: FxHashMap, MonoFunction<'db>>, + pub(super) synthetic: FxHashMap, String>, + pub(super) synthetic_order: Vec>, + pub(super) synthetic_funs: FxHashMap, MonoFunction<'db>>, + pub(super) queue: VecDeque>, + pub(super) dispatch_selector_overrides: Vec<(String, String)>, + pub(super) diagnostics: Vec>, +} + +#[derive(Debug, Clone)] +pub(super) struct FunctionInfo<'db> { + pub(super) module: Module<'db>, + pub(super) function: FunctionDef<'db>, + pub(super) body: Option>, + pub(super) type_vars: Vec>, + pub(super) kind: FunctionInfoKind<'db>, +} + +#[derive(Debug, Clone)] +pub(super) enum FunctionInfoKind<'db> { + Source, + Contract, + InstanceMethod { + instance: DefId<'db>, + method: String, + }, +} + +#[derive(Debug, Clone)] +pub(super) struct InstanceInfo<'db> { + pub(super) instance: InstanceDef<'db>, + pub(super) head: Pred<'db>, + pub(super) preds: Vec>, +} + +#[derive(Debug, Clone)] +pub(super) struct ClassInfo<'db> { + pub(super) module: Module<'db>, + pub(super) class: hir::ast::item::ClassDef<'db>, +} + +#[derive(Debug, Clone)] +pub(super) struct AdtInfo<'db> { + pub(super) module: Module<'db>, + pub(super) adt: AdtDef<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct SpecKey<'db> { + pub(super) def: DefId<'db>, + pub(super) ty: Ty<'db>, + pub(super) base_name: String, + pub(super) origin: MonoFunctionOrigin<'db>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct SyntheticKey<'db> { + pub(super) adt: DefId<'db>, + pub(super) method: String, + pub(super) main: Ty<'db>, + pub(super) rep: Ty<'db>, +} + +#[derive(Debug, Clone)] +pub(super) struct PendingSpec<'db> { + pub(super) key: SpecKey<'db>, + pub(super) depth: usize, +} + +impl<'db> Driver<'db> { + pub(super) fn new( + db: &'db dyn Db, + prepared: PreparedModule<'db>, + options: SpecializeOptions, + ) -> Self { + let module = prepared.module(db); + let entry_module = module_id_for_source_file(db, module.def_id_value(db).file(db)); + let modules = reachable_modules(db, module); + let mut module_resolutions = FxHashMap::default(); + let mut module_trait_envs = FxHashMap::default(); + for indexed in &modules { + let resolution = resolve_specialize_module(db, *indexed); + let trait_env = specialization_trait_env(db, *indexed, &resolution); + module_resolutions.insert(indexed.def_id_value(db), resolution); + module_trait_envs.insert(indexed.def_id_value(db), trait_env); + } + let mut driver = Self { + db, + prepared, + module, + entry_module, + modules, + options, + module_resolutions, + module_trait_envs, + functions: FxHashMap::default(), + body_maps: FxHashMap::default(), + classes: FxHashMap::default(), + instances: FxHashMap::default(), + adts: FxHashMap::default(), + specs: FxHashMap::default(), + spec_order: Vec::new(), + mono_funs: FxHashMap::default(), + synthetic: FxHashMap::default(), + synthetic_order: Vec::new(), + synthetic_funs: FxHashMap::default(), + queue: VecDeque::new(), + dispatch_selector_overrides: Vec::new(), + diagnostics: Vec::new(), + }; + driver.collect_module_index(); + driver.collect_body_maps(); + driver + } + + pub(super) fn run(&mut self) -> SpecializeOutput<'db> { + let (contracts, roots) = self.collect_roots(); + let root_names = roots + .iter() + .map(|root| root.base_name.clone()) + .collect::>(); + for root in roots { + self.enqueue(root, 0); + } + while let Some(pending) = self.queue.pop_front() { + self.specialize_pending(pending); + } + + let mut items = Vec::new(); + for contract in contracts { + items.push(MonoItem::Contract(contract)); + } + for adt in self.adts.keys() { + items.push(MonoItem::Adt(*adt)); + } + for key in &self.spec_order { + if let Some(fun) = self.mono_funs.get(key) { + items.push(MonoItem::Function(fun.clone())); + } + } + for key in &self.synthetic_order { + if let Some(fun) = self.synthetic_funs.get(key) { + items.push(MonoItem::Function(fun.clone())); + } + } + + let module = MonoModule { + module: self.module.def_id_value(self.db), + frontend_desugar: frontend_desugar_plan(self.db, self.module), + entry_points: root_names, + items, + }; + let (mut module, mut eval_diagnostics) = evaluate_module( + self.db, + module, + EvaluateOptions { + fuel: self.options.eval_fuel, + inline_depth: self.options.max_depth, + }, + ); + patch_runtime_dispatch_selectors(&mut module, &self.dispatch_selector_overrides); + self.diagnostics.append(&mut eval_diagnostics); + + SpecializeOutput { + module, + diagnostics: std::mem::take(&mut self.diagnostics), + } + } + + fn collect_module_index(&mut self) { + let modules = self.modules.clone(); + for module in modules { + let items = module.items(self.db).clone(); + for item in items { + self.collect_item(module, item, &[]); + } + } + } + + fn collect_body_maps(&mut self) { + let modules = self.modules.clone(); + for module in modules { + let mut bodies = Vec::new(); + for item in module.items(self.db) { + collect_body_order(self.db, *item, &mut bodies); + } + let Some(resolution) = self.module_resolutions.get(&module.def_id_value(self.db)) + else { + continue; + }; + for (body, map) in bodies.into_iter().zip(resolution.bodies.iter().cloned()) { + self.body_maps.insert(body, map); + } + } + } + + fn collect_item( + &mut self, + module: Module<'db>, + item: Item<'db>, + inherited: &[hir_nameres::TypeVarBinding<'db>], + ) { + match item { + Item::FunctionDef(function) => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + function.def_id_value(self.db), + &function.sig(self.db).type_vars, + )); + self.functions.insert( + function.def_id_value(self.db), + FunctionInfo { + module, + function, + body: function.body(self.db), + type_vars, + kind: FunctionInfoKind::Source, + }, + ); + } + Item::ContractDef(contract) => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + contract.def_id_value(self.db), + contract.ty_param_elems(self.db), + )); + for item in contract.items(self.db) { + match *item { + ContractItem::FunctionDef(function) => { + let mut fn_type_vars = type_vars.clone(); + fn_type_vars.extend(type_var_bindings( + function.def_id_value(self.db), + &function.sig(self.db).type_vars, + )); + self.functions.insert( + function.def_id_value(self.db), + FunctionInfo { + module, + function, + body: function.body(self.db), + type_vars: fn_type_vars, + kind: FunctionInfoKind::Contract, + }, + ); + } + ContractItem::AdtDef(adt) => { + self.adts + .insert(adt.def_id_value(self.db), AdtInfo { module, adt }); + } + ContractItem::TypeAlias(_) | ContractItem::Error { .. } => {} + } + } + } + Item::InstanceDef(instance) => { + let mut type_vars = inherited.to_vec(); + type_vars.extend(type_var_bindings( + instance.def_id_value(self.db), + instance.type_var_elems(self.db), + )); + let Some(head) = self.try_lower_pred_with_vars( + module, + instance.head(self.db), + &type_vars, + Some(instance.span(self.db)), + ) else { + return; + }; + let Some(preds) = instance + .preds(self.db) + .iter() + .map(|pred| { + self.try_lower_pred_with_vars( + module, + *pred, + &type_vars, + Some(instance.span(self.db)), + ) + }) + .collect::>>() + else { + return; + }; + self.instances.insert( + instance.def_id_value(self.db), + InstanceInfo { + instance, + head, + preds, + }, + ); + for method in instance.methods(self.db) { + let method_name = ident_text(self.db, &method.sig(self.db).name); + let mut method_type_vars = type_vars.clone(); + method_type_vars.extend(type_var_bindings( + method.def_id_value(self.db), + &method.sig(self.db).type_vars, + )); + self.functions.insert( + method.def_id_value(self.db), + FunctionInfo { + module, + function: *method, + body: method.body(self.db), + type_vars: method_type_vars, + kind: FunctionInfoKind::InstanceMethod { + instance: instance.def_id_value(self.db), + method: method_name, + }, + }, + ); + } + } + Item::AdtDef(adt) => { + self.adts + .insert(adt.def_id_value(self.db), AdtInfo { module, adt }); + } + Item::ClassDef(class) => { + self.classes + .insert(class.def_id_value(self.db), ClassInfo { module, class }); + } + Item::TypeAlias(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } + } + + fn collect_roots(&mut self) -> (Vec>, Vec>) { + let mut contracts = Vec::new(); + let mut roots = Vec::new(); + let mut has_contract = false; + for item in self.module.items(self.db) { + let Item::ContractDef(contract) = item else { + continue; + }; + if contract.kind(self.db) != ContractKind::Contract { + continue; + } + has_contract = true; + // Constructor declarations are replaced by deployment wrappers in + // effective HIR. Keep ABI/source metadata anchored to the source + // contract while roots come from the prepared contract. + let source_module = self.prepared.source(self.db); + let source_contract = source_module + .items(self.db) + .iter() + .find_map(|item| match item { + Item::ContractDef(source) + if source.def_id_value(self.db) == contract.def_id_value(self.db) => + { + Some(*source) + } + _ => None, + }); + let (surface_module, surface_contract) = + source_contract.map_or((self.module, *contract), |source| (source_module, source)); + let surface = + contract_dispatch_surface_for_module(self.db, surface_module, surface_contract); + let constructor_surface = surface.constructor.clone(); + let fallback_surface = surface.fallback.clone(); + let mut entries = Vec::new(); + let mut blocked_runtime_entry = false; + for method in &surface.methods { + if let Some(info) = self.functions.get(&method.def).cloned() + && self.reject_abi_visible_comptime_params(&info) + { + blocked_runtime_entry = true; + } + } + let mut constructor_meta = MonoConstructor { + source: None, + explicit: matches!(constructor_surface, DispatchConstructor::Explicit { .. }), + payable: match &constructor_surface { + DispatchConstructor::Implicit => false, + DispatchConstructor::Explicit { payable, .. } => *payable, + }, + inputs: match &constructor_surface { + DispatchConstructor::Implicit => Vec::new(), + DispatchConstructor::Explicit { inputs, .. } => mono_abi_params(inputs.clone()), + }, + span: contract.span(self.db), + }; + let mut fallback_meta = MonoFallback { + source: match &fallback_surface { + DispatchFallback::Default => None, + DispatchFallback::Explicit { def, .. } => Some(*def), + }, + explicit: matches!(fallback_surface, DispatchFallback::Explicit { .. }), + specialized: None, + payable: match &fallback_surface { + DispatchFallback::Default => false, + DispatchFallback::Explicit { payable, .. } => *payable, + }, + inputs: match &fallback_surface { + DispatchFallback::Default => Vec::new(), + DispatchFallback::Explicit { inputs, .. } => mono_abi_params(inputs.clone()), + }, + outputs: match &fallback_surface { + DispatchFallback::Default => Vec::new(), + DispatchFallback::Explicit { outputs, .. } => mono_abi_params(outputs.clone()), + }, + span: contract.span(self.db), + }; + let runtime_main = contract.items(self.db).iter().find_map(|item| { + let ContractItem::FunctionDef(function) = *item else { + return None; + }; + let def = function.def_id_value(self.db); + self.runtime_main_origin(function, def) + .map(|origin| (function, origin)) + }); + if runtime_main + .as_ref() + .is_some_and(|(_, origin)| *origin == MonoRuntimeMainOrigin::StdDispatch) + { + for method in &surface.methods { + self.dispatch_selector_overrides.push(( + format!( + "DispatchNameTy_{}_{}", + ident_text(self.db, &contract.name_elem(self.db)), + method.name + ), + u32::from_be_bytes(method.selector.0).to_string(), + )); + } + } + let deployment_main = contract.items(self.db).iter().find_map(|item| { + let ContractItem::FunctionDef(function) = *item else { + return None; + }; + let def = function.def_id_value(self.db); + (self + .prepared + .origin_for_def(self.db, def) + .is_some_and(|origin| { + origin.kind == GeneratedOriginKind::ContractDeploymentMain + }) + || is_contract_deployment_main_def(self.db, def)) + .then_some(function) + }); + if let DispatchConstructor::Explicit { source_index, .. } = &constructor_surface + && let Some(ContractItem::FunctionDef(function)) = + surface_contract.items(self.db).get(*source_index) + { + constructor_meta.source = Some(function.def_id_value(self.db)); + constructor_meta.span = function.span(self.db); + } + if let Some(function) = deployment_main + && let Some(key) = self.root_for_def(function.def_id_value(self.db)) + { + entries.push(MonoEntry::DeploymentMain { + source: function.def_id_value(self.db), + specialized: key.base_name.clone(), + span: function.span(self.db), + }); + roots.push(key); + } + if let DispatchFallback::Explicit { + def, + payable, + inputs, + outputs, + .. + } = &fallback_surface + && let Some(key) = self.root_for_def(*def) + { + fallback_meta.specialized = Some(key.base_name.clone()); + fallback_meta.span = self + .functions + .get(def) + .map(|info| info.function.span(self.db)) + .unwrap_or_else(|| contract.span(self.db)); + entries.push(MonoEntry::Fallback { + source: *def, + specialized: key.base_name.clone(), + span: self + .functions + .get(def) + .map(|info| info.function.span(self.db)) + .unwrap_or_else(|| contract.span(self.db)), + payable: *payable, + inputs: mono_abi_params(inputs.clone()), + outputs: mono_abi_params(outputs.clone()), + }); + roots.push(key); + } + if !blocked_runtime_entry + && let Some((function, origin)) = runtime_main + && let Some(key) = self.root_for_def(function.def_id_value(self.db)) + { + entries.push(MonoEntry::RuntimeMain { + source: function.def_id_value(self.db), + specialized: key.base_name.clone(), + span: function.span(self.db), + origin, + }); + roots.push(key); + } + contracts.push(MonoContract { + def: contract.def_id_value(self.db), + name: ident_text(self.db, &contract.name_elem(self.db)), + span: contract.span(self.db), + constructor: constructor_meta, + fallback: fallback_meta, + entries, + }); + } + + if !has_contract { + let main_defs = self + .functions + .values() + .filter(|info| { + matches!(&info.kind, FunctionInfoKind::Source) + && info.module.def_id_value(self.db) == self.module.def_id_value(self.db) + && ident_text(self.db, &info.function.sig(self.db).name) == "main" + }) + .map(|info| info.function.def_id_value(self.db)) + .collect::>(); + for def in main_defs { + if let Some(key) = self.root_for_def(def) { + roots.push(key); + } + } + } + + (contracts, roots) + } + + fn reject_abi_visible_comptime_params(&mut self, info: &FunctionInfo<'db>) -> bool { + let function = ident_text(self.db, &info.function.sig(self.db).name); + let mut rejected = false; + for param in info.function.sig(self.db).params.atom() { + if !param_comptime(param) { + continue; + } + let param_name = param_name(self.db, param).unwrap_or("_").to_owned(); + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::PublicComptimeParam { + function: function.clone(), + param: param_name, + }, + span: Some(param.span(self.db)), + }); + rejected = true; + } + rejected + } + + fn runtime_main_origin( + &self, + function: FunctionDef<'db>, + def: DefId<'db>, + ) -> Option { + if self + .prepared + .origin_for_def(self.db, def) + .is_some_and(|origin| origin.kind == GeneratedOriginKind::ContractDispatchMain) + || is_contract_dispatch_main_def(self.db, def) + { + return Some(MonoRuntimeMainOrigin::StdDispatch); + } + (function.kind(self.db) == FuncKind::Function + && ident_text(self.db, &function.sig(self.db).name) == "main") + .then_some(MonoRuntimeMainOrigin::User) + } + + fn root_for_def(&mut self, def: DefId<'db>) -> Option> { + let info = self.functions.get(&def).cloned()?; + let lowered = self.try_lower_normalized_function(&info)?; + let ty = lowered.scheme.body(self.db).ty(self.db); + let span = info.function.span(self.db); + if !self.ensure_closed(ty, "entry specialization", Some(span)) { + return None; + } + let base = self.source_base_name(&info); + let name = specialize_name(self.db, &base, &[]); + Some(SpecKey { + def, + ty, + base_name: name, + origin: MonoFunctionOrigin::Source, + }) + } + + pub(super) fn enqueue(&mut self, key: SpecKey<'db>, depth: usize) -> String { + if let Some(name) = self.specs.get(&key) { + return name.clone(); + } + if !self.ensure_specialization_type_size(&[key.ty], None) { + return key.base_name; + } + if self.specs.len() >= self.options.max_instantiations { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::InstantiationFuelExhausted { + limit: self.options.max_instantiations, + }, + span: None, + }); + return key.base_name; + } + if depth > self.options.max_depth { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::InstantiationDepthExceeded { + limit: self.options.max_depth, + }, + span: None, + }); + return key.base_name; + } + let name = key.base_name.clone(); + self.specs.insert(key.clone(), name.clone()); + self.spec_order.push(key.clone()); + self.queue.push_back(PendingSpec { key, depth }); + name + } + + fn specialize_pending(&mut self, pending: PendingSpec<'db>) { + if self.mono_funs.contains_key(&pending.key) { + return; + } + let Some(info) = self.functions.get(&pending.key.def).cloned() else { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::UnresolvedExternal { + function: pending.key.def, + name: pending.key.base_name, + }, + span: None, + }); + return; + }; + let Some(body) = info.body else { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingBody { + function: pending.key.def, + }, + span: Some(info.function.span(self.db)), + }); + return; + }; + let Some(lowered) = self.try_lower_normalized_function(&info) else { + return; + }; + let mut subst = TySubst::default(); + if !subst.match_ty( + self.db, + lowered.scheme.body(self.db).ty(self.db), + pending.key.ty, + ) { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: format!( + "cannot match {} against {}", + lowered.scheme.body(self.db).ty(self.db).display(self.db), + pending.key.ty.display(self.db) + ), + }, + span: Some(info.function.span(self.db)), + }); + return; + } + let givens = self.function_givens(&info, &lowered); + self.resolve_mptc_from_preds(info.module, &givens, &mut subst); + let Some(params) = self.function_params(&info, &lowered, &subst, pending.key.ty) else { + return; + }; + let ret = self.specialized_return_ty(&info, &lowered, &subst, pending.key.ty); + if !self.ensure_closed( + ret, + &pending.key.base_name, + Some(info.function.span(self.db)), + ) { + return; + } + if self.reject_unsupported_runtime_type( + ret, + "function return type", + info.function.span(self.db), + ) { + return; + } + let Some(body_map) = self.body_resolution_for(body).cloned() else { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: format!("missing body resolution for {}", pending.key.base_name), + }, + span: Some(info.function.span(self.db)), + }); + return; + }; + let pre_typeck_desugar = hir_ty::pre_typeck_desugar_body_tree(self.db, body); + let Some(result) = + self.try_infer_result(&info, body, &body_map, &lowered, pre_typeck_desugar.clone()) + else { + return; + }; + let shadowed_top_level = self.shadowed_top_level_function(&info); + let index = Arc::new(BodyIndex::new(self.db, &result, &body_map)); + let mut ctx = BodyCtx { + driver: self, + info: &info, + body, + result, + body_map, + pre_typeck_desugar, + subst, + depth: pending.depth, + index, + lowered_exprs: FxHashMap::default(), + locals: params + .iter() + .map(|param| (param.name.clone(), param.ty.ty())) + .collect(), + }; + let Some(body) = body + .top_level_stmts(ctx.driver.db) + .iter() + .map(|stmt| ctx.stmt(*stmt)) + .collect::>>() + else { + return; + }; + let Some(comptime_obligations) = ctx.comptime_obligations() else { + return; + }; + let fun = MonoFunction { + origin: pending.key.origin.clone(), + source: Some(pending.key.def), + shadowed_top_level, + name: pending.key.base_name.clone(), + span: info.function.span(ctx.driver.db), + params, + ret: MonoTy::new_unchecked(ret), + comptime_obligations, + body, + }; + ctx.driver.mono_funs.insert(pending.key, fun); + } + + fn shadowed_top_level_function(&self, info: &FunctionInfo<'db>) -> Option { + if !matches!(info.kind, FunctionInfoKind::Contract) { + return None; + } + let name = ident_text(self.db, &info.function.sig(self.db).name); + let has_top_level_function = self.functions.values().any(|candidate| { + matches!(candidate.kind, FunctionInfoKind::Source) + && candidate.module.def_id_value(self.db) == info.module.def_id_value(self.db) + && ident_text(self.db, &candidate.function.sig(self.db).name) == name + }); + has_top_level_function.then_some(name) + } + + fn function_params( + &mut self, + info: &FunctionInfo<'db>, + lowered: &LoweredFunction<'db>, + subst: &TySubst<'db>, + key_ty: Ty<'db>, + ) -> Option>> { + let sig = info.function.sig(self.db); + let params = sig.params.atom(); + if params.len() != lowered.params.len() { + return None; + } + let mut out = Vec::new(); + for (index, (param, ty)) in params.iter().zip(&lowered.params).enumerate() { + let ty = self.specialized_param_ty(*ty, subst, key_ty, index); + if !self.ensure_closed(ty, "parameter", Some(param.span(self.db))) { + return None; + } + if self.reject_unsupported_runtime_type(ty, "function parameter", param.span(self.db)) { + return None; + } + out.push(MonoParam { + name: param_name(self.db, param).unwrap_or("_").to_owned(), + mode: ParamMode::from_bool(param_comptime(param) || ty_is_comptime(self.db, ty)), + ty: MonoTy::new_unchecked(ty), + span: param.span(self.db), + }); + } + Some(out) + } + + fn specialized_return_ty( + &self, + info: &FunctionInfo<'db>, + lowered: &LoweredFunction<'db>, + subst: &TySubst<'db>, + key_ty: Ty<'db>, + ) -> Ty<'db> { + let ret = subst.apply_ty(self.db, lowered.ret); + if info.function.sig(self.db).ret.is_none() + && !ty_is_closed(self.db, ret) + && let Some(key_ret) = function_ret_ty(self.db, key_ty) + && ty_is_closed(self.db, key_ret) + { + return key_ret; + } + ret + } + + fn specialized_param_ty( + &self, + lowered_param: Ty<'db>, + subst: &TySubst<'db>, + key_ty: Ty<'db>, + index: usize, + ) -> Ty<'db> { + let ty = subst.apply_ty(self.db, lowered_param); + if !ty_is_closed(self.db, ty) + && let Some(key_param) = function_param_ty(self.db, key_ty, index) + && ty_is_closed(self.db, key_param) + { + return key_param; + } + ty + } + + pub(super) fn source_base_name(&self, info: &FunctionInfo<'db>) -> String { + match &info.kind { + FunctionInfoKind::Source | FunctionInfoKind::Contract => { + self.qualified_source_base_name(info) + } + FunctionInfoKind::InstanceMethod { method, .. } => method.clone(), + } + } + + fn qualified_source_base_name(&self, info: &FunctionInfo<'db>) -> String { + let def = info.function.def_id_value(self.db); + let mut parts = def_owner_path(self.db, def); + parts.push( + contract_overlay_backend_name(self.db, def) + .map(str::to_owned) + .unwrap_or_else(|| ident_text(self.db, &info.function.sig(self.db).name)), + ); + parts.push(def_hash_suffix(self.db, def)); + join_sanitized_name_components(parts) + } + + pub(super) fn call_origin_for_def(&self, def: DefId<'db>) -> MonoCallOrigin<'db> { + self.std_intrinsic_for_def(def) + .map(MonoCallOrigin::Builtin) + .unwrap_or(MonoCallOrigin::Source(def)) + } + + fn std_intrinsic_for_def(&self, def: DefId<'db>) -> Option { + let path = hir::url_to_file_path(def.file(self.db).url(self.db))?; + let std_key = module_key_for_path( + LibraryId::Std, + self.db.module_tree().std_root(self.db), + &path, + )?; + if std_key.logical_path.as_slice() != ["std"] { + return None; + } + match def.name(self.db).as_deref()? { + "addWord" => Some(MonoIntrinsic::PrimAddWord), + "subWord" => Some(MonoIntrinsic::SubWord), + "mulWord" => Some(MonoIntrinsic::MulWord), + "gtWord" => Some(MonoIntrinsic::GtWord), + "bxorWord" => Some(MonoIntrinsic::BxorWord), + "bandWord" => Some(MonoIntrinsic::BandWord), + "borWord" => Some(MonoIntrinsic::BorWord), + "eqWord" => Some(MonoIntrinsic::PrimEqWord), + "concatLit" => Some(MonoIntrinsic::ConcatLit), + "strlenLit" => Some(MonoIntrinsic::StrlenLit), + "keccakLit" => Some(MonoIntrinsic::KeccakLit), + _ => None, + } + } + + pub(super) fn std_intrinsic_named(&self, name: &str) -> Option { + self.functions.iter().find_map(|(def, info)| { + (ident_text(self.db, &info.function.sig(self.db).name) == name) + .then(|| self.std_intrinsic_for_def(*def)) + .flatten() + }) + } + + pub(super) fn unique_class_named(&self, name: &str) -> Option> { + let mut matches = self.classes.iter().filter_map(|(def, info)| { + (ident_text(self.db, &info.class.head(self.db).kind(self.db).class) == name) + .then_some(*def) + }); + let first = matches.next()?; + matches.next().is_none().then_some(first) + } + + pub(super) fn try_lower_normalized_function( + &mut self, + info: &FunctionInfo<'db>, + ) -> Option> { + let Some(resolution) = self.try_module_resolution(info.module) else { + self.push_missing_module_resolution(Some(info.function.span(self.db))); + return None; + }; + let body_map = info.body.and_then(|body| self.body_resolution_for(body)); + Some(lower_normalized_function_with_inferred_signature( + self.db, + info.module, + &resolution.item_resolutions, + info.function, + &info.type_vars, + body_map, + self.entry_module, + )) + } + + pub(super) fn function_givens( + &self, + info: &FunctionInfo<'db>, + lowered: &LoweredFunction<'db>, + ) -> Vec> { + let mut givens = lowered.scheme.body(self.db).preds(self.db).clone(); + if let FunctionInfoKind::InstanceMethod { instance, .. } = &info.kind + && let Some(instance) = self.instances.get(instance) + { + givens.extend(instance.preds.iter().copied()); + } + givens + } + + fn try_lower_pred_with_vars( + &mut self, + module: Module<'db>, + pred: hir::ast::ty::PredRef<'db>, + type_vars: &[hir_nameres::TypeVarBinding<'db>], + span: Option>, + ) -> Option> { + let Some(resolution) = self.try_module_resolution(module) else { + self.push_missing_module_resolution(span); + return None; + }; + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(type_vars), + ); + let mut normalizer = AliasNormalizer::new(self.db, module, &resolution.item_resolutions); + Some(normalizer.normalize_pred(lowerer.lower_pred(pred))) + } + + pub(super) fn try_module_resolution( + &self, + module: Module<'db>, + ) -> Option<&hir_nameres::ModuleResolutionMap<'db>> { + let resolution = self.module_resolutions.get(&module.def_id_value(self.db)); + debug_assert!(resolution.is_some(), "module resolution indexed"); + resolution + } + + pub(super) fn try_module_trait_env( + &self, + module: Module<'db>, + ) -> Option> { + let trait_env = self.module_trait_envs.get(&module.def_id_value(self.db)); + debug_assert!(trait_env.is_some(), "module trait environment indexed"); + trait_env.copied() + } + + pub(super) fn push_missing_module_resolution(&mut self, span: Option>) { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: "module resolution".to_owned(), + }, + span, + }); + } + + pub(super) fn push_missing_module_trait_env(&mut self, span: Option>) { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::MissingResolution { + context: "module trait environment".to_owned(), + }, + span, + }); + } + + fn try_infer_result( + &mut self, + info: &FunctionInfo<'db>, + body: FuncBody<'db>, + body_map: &hir_nameres::BodyResolutionMap<'db>, + lowered: &LoweredFunction<'db>, + pre_typeck_desugar: Vec>, + ) -> Option> { + let Some(module_trait_env) = self.try_module_trait_env(info.module) else { + self.push_missing_module_trait_env(Some(info.function.span(self.db))); + return None; + }; + let trait_env = trait_env_with_givens( + self.db, + module_trait_env, + self.function_givens(info, lowered), + ); + let ctx = BodyTyContext::new( + info.module, + body_map.clone(), + info.type_vars.clone(), + lowered.params.clone(), + Some(lowered.ret), + ) + .with_param_names(param_names( + self.db, + info.function.sig(self.db).params.atom(), + )) + .with_trait_env(trait_env) + .with_pre_typeck_desugar(pre_typeck_desugar); + let ctx = if let Some(entry_module) = self.entry_module { + ctx.with_entry_module(entry_module) + } else { + ctx + }; + Some(infer_body(self.db, body, ctx)) + } + + pub(super) fn body_resolution_for( + &self, + body: FuncBody<'db>, + ) -> Option<&hir_nameres::BodyResolutionMap<'db>> { + self.body_maps.get(&body).or_else(|| { + self.module_resolutions.values().find_map(|resolution| { + resolution + .bodies + .iter() + .find(|candidate| body_map_contains(candidate, body)) + }) + }) + } + + fn ensure_closed(&mut self, ty: Ty<'db>, context: &str, span: Option>) -> bool { + if ty_is_closed(self.db, ty) { + true + } else { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::FreeTypeVariable { + context: context.to_owned(), + ty: display_backend_ty(self.db, ty), + }, + span, + }); + false + } + } + + pub(super) fn ensure_specialization_type_size( + &mut self, + tys: &[Ty<'db>], + span: Option>, + ) -> bool { + if tys + .iter() + .any(|ty| ty_node_budget_exceeded(self.db, *ty, self.options.max_type_nodes)) + { + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::TypeSizeExceeded { + limit: self.options.max_type_nodes, + }, + span, + }); + false + } else { + true + } + } + + pub(super) fn mono_ty( + &mut self, + ty: Ty<'db>, + context: &str, + span: Span<'db>, + ) -> Option> { + if self.reject_unsupported_runtime_type(ty, context, span) { + return None; + } + self.ensure_closed(ty, context, Some(span)) + .then(|| MonoTy::new_unchecked(ty)) + } + + fn reject_unsupported_runtime_type( + &mut self, + ty: Ty<'db>, + context: &str, + span: Span<'db>, + ) -> bool { + if !self.ty_contains_fixed_array_in_layout(ty, &mut FxHashSet::default()) { + return false; + } + self.diagnostics.push(SpecializeDiagnostic { + kind: SpecializeDiagnosticKind::UnsupportedRuntimeType { + context: context.to_owned(), + ty: display_backend_ty(self.db, ty), + }, + span: Some(span), + }); + true + } + + fn ty_contains_fixed_array_in_layout( + &self, + ty: Ty<'db>, + visiting: &mut FxHashSet>, + ) -> bool { + match ty.kind(self.db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::FixedArray(_)), + .. + } => true, + TyKind::Named { ctor, args } => { + if args + .iter() + .any(|arg| self.ty_contains_fixed_array_in_layout(*arg, visiting)) + { + return true; + } + let TyCtor::User(user) = ctor else { + return false; + }; + if user.kind != UserTyCtorKind::Adt || !visiting.insert(user.def) { + return false; + } + let contains = self + .adt_fixed_array_field_types(user.def, args) + .into_iter() + .any(|field| self.ty_contains_fixed_array_in_layout(field, visiting)); + visiting.remove(&user.def); + contains + } + TyKind::Function { params, ret } => { + params + .iter() + .any(|param| self.ty_contains_fixed_array_in_layout(*param, visiting)) + || self.ty_contains_fixed_array_in_layout(*ret, visiting) + } + TyKind::Tuple(args) => args + .iter() + .any(|arg| self.ty_contains_fixed_array_in_layout(*arg, visiting)), + TyKind::Comptime(inner) => self.ty_contains_fixed_array_in_layout(*inner, visiting), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => false, + } + } + + fn adt_fixed_array_field_types(&self, def: DefId<'db>, args: &[Ty<'db>]) -> Vec> { + let Some(info) = self.adts.get(&def) else { + return Vec::new(); + }; + let Some(resolution) = self + .module_resolutions + .get(&info.module.def_id_value(self.db)) + else { + return Vec::new(); + }; + let type_vars = type_var_bindings( + info.adt.def_id_value(self.db), + info.adt.ty_param_elems(self.db), + ); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(&type_vars), + ); + let mut fields = Vec::new(); + for ctor in info.adt.ctors(self.db) { + let lowered = lowerer.lower_adt_ctor(info.adt, ctor); + let mut normalizer = + AliasNormalizer::new(self.db, info.module, &resolution.item_resolutions); + fields.extend(lowered.params.into_iter().map(|field| { + let field = normalizer.normalize_ty(field); + substitute_bound_ty(self.db, field, args) + })); + } + fields + } +} + +fn substitute_bound_ty<'db>(db: &'db dyn Db, ty: Ty<'db>, args: &[Ty<'db>]) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => args.get(var.index as usize).copied().unwrap_or(ty), + TyKind::Named { ctor, args: inner } => Ty::named( + db, + *ctor, + inner + .iter() + .map(|arg| substitute_bound_ty(db, *arg, args)) + .collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| substitute_bound_ty(db, *param, args)) + .collect(), + substitute_bound_ty(db, *ret, args), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems + .iter() + .map(|elem| substitute_bound_ty(db, *elem, args)) + .collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, substitute_bound_ty(db, *inner, args)), + TyKind::Error | TyKind::Unknown => ty, + } +} + +fn patch_runtime_dispatch_selectors(module: &mut MonoModule<'_>, selectors: &[(String, String)]) { + for item in &mut module.items { + let MonoItem::Function(function) = item else { + continue; + }; + if !function.name.starts_with("dispatch_selector_matches") { + continue; + } + let Some((_, selector)) = selectors + .iter() + .find(|(marker, _)| function.name.contains(marker)) + else { + continue; + }; + let _ = replace_selector_candidate(&mut function.body, selector); + } +} + +fn replace_selector_candidate(stmts: &mut [MonoStmt<'_>], replacement: &str) -> bool { + for stmt in stmts { + let replaced = match &mut stmt.kind { + MonoStmtKind::Let { id, init, .. } if id.name == "candidate" => init + .as_mut() + .is_some_and(|expr| replace_selector_value(expr, replacement)), + MonoStmtKind::Assign { lhs, rhs, .. } if matches!(&lhs.kind, MonoExprKind::Var(id) if id.name == "candidate") => { + replace_selector_value(rhs, replacement) + } + MonoStmtKind::Match { arms, .. } => arms + .iter_mut() + .any(|arm| replace_selector_candidate(&mut arm.body, replacement)), + MonoStmtKind::For { + init, post, body, .. + } => { + replace_selector_candidate(init, replacement) + || replace_selector_candidate(post, replacement) + || replace_selector_candidate(body, replacement) + } + MonoStmtKind::If { + then_body, + else_body, + .. + } => { + replace_selector_candidate(then_body, replacement) + || else_body + .as_mut() + .is_some_and(|body| replace_selector_candidate(body, replacement)) + } + MonoStmtKind::Block(body) => replace_selector_candidate(body, replacement), + MonoStmtKind::Let { .. } + | MonoStmtKind::Return(_) + | MonoStmtKind::Expr(_) + | MonoStmtKind::Assign { .. } + | MonoStmtKind::Assembly(_) + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Error => false, + }; + if replaced { + return true; + } + } + false +} + +fn replace_selector_value(expr: &mut MonoExpr<'_>, replacement: &str) -> bool { + match &mut expr.kind { + MonoExprKind::Lit(hir::ast::function::LitKind::Number(value)) => { + *value = replacement.to_owned(); + true + } + MonoExprKind::Conversion { expr, .. } => replace_selector_value(expr, replacement), + _ => false, + } +} diff --git a/crates/specialize/src/specialize/evidence.rs b/crates/specialize/src/specialize/evidence.rs new file mode 100644 index 00000000..f4d5f8a4 --- /dev/null +++ b/crates/specialize/src/specialize/evidence.rs @@ -0,0 +1,376 @@ +use super::*; + +impl<'db> Driver<'db> { + pub(super) fn resolve_class_method_call( + &mut self, + method: &str, + evidence: Evidence<'db>, + target_ty: Ty<'db>, + call_span: Span<'db>, + depth: usize, + ) -> Option { + match evidence { + Evidence::Instance { + instance, + args, + sub_evidence: _, + } => { + let info = self.instances.get(&instance)?.clone(); + let method_def = info.instance.methods(self.db).iter().find(|candidate| { + ident_text(self.db, &candidate.sig(self.db).name) == method + })?; + let subst = TySubst::from_args(args); + let head = subst.apply_pred(self.db, info.head); + let (class_name, head_tys) = class_method_name_parts(self.db, head); + if !self.ensure_specialization_type_size(&head_tys, Some(call_span)) + || !self.ensure_specialization_type_size(&[target_ty], Some(call_span)) + { + return None; + } + let base = specialize_name( + self.db, + &format!( + "{class_name}_{method}_{}", + def_hash_suffix(self.db, method_def.def_id_value(self.db)) + ), + head_tys.as_slice(), + ); + let key = SpecKey { + def: method_def.def_id_value(self.db), + ty: target_ty, + base_name: base, + origin: MonoFunctionOrigin::InstanceMethod { + instance, + class: class_name, + method: method.to_owned(), + }, + }; + Some(self.enqueue(key, depth + 1)) + } + Evidence::Superclass { pred, child, .. } => { + if let Some(evidence) = self.solve_closed_pred(pred, Some(call_span)) + && !matches!(evidence, Evidence::Superclass { .. }) + { + return self + .resolve_class_method_call(method, evidence, target_ty, call_span, depth); + } + self.resolve_class_method_call(method, *child, target_ty, call_span, depth) + } + Evidence::Derived { + kind: DerivedClauseKind::Generic { adt }, + pred, + .. + } => { + let PredKind::InClass { main, args, .. } = pred.kind(self.db) else { + return None; + }; + let rep = args.first().copied()?; + self.specialize_derived_generic(adt, method, *main, rep, target_ty, call_span) + } + Evidence::Builtin { pred } => { + let method_evidence = match pred.kind(self.db) { + PredKind::InClass { + class: ClassId::User(class), + .. + } => self.solve_class_method_pred(*class, method, target_ty, Some(call_span)), + _ => None, + }; + let replacement = method_evidence.or_else(|| { + self.solve_closed_pred(pred, Some(call_span)) + .or_else(|| self.solve_reachable_pred(pred, Some(call_span))) + .or_else(|| self.derived_generic_evidence(pred)) + }); + if let Some(evidence) = replacement + && !matches!(evidence, Evidence::Builtin { .. }) + { + return self + .resolve_class_method_call(method, evidence, target_ty, call_span, depth); + } + None + } + Evidence::Derived { .. } => None, + } + } + + fn derived_generic_evidence(&self, pred: Pred<'db>) -> Option> { + let PredKind::InClass { + class: ClassId::User(class), + main, + args: class_args, + } = pred.kind(self.db) + else { + return None; + }; + if class.name(self.db).as_deref() != Some("Generic") || class_args.len() != 1 { + return None; + } + let TyKind::Named { + ctor: + TyCtor::User(UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } = main.kind(self.db) + else { + return None; + }; + let info = self.adts.get(def)?; + let plan = derived_generic_instance_plan(self.db, info.module, info.adt, *class)?; + let rep = TySubst::from_args(args.clone()).apply_ty(self.db, plan.rep); + (rep == class_args[0]).then_some(Evidence::Derived { + kind: DerivedClauseKind::Generic { adt: *def }, + pred, + sub_evidence: Vec::new(), + }) + } + + fn solve_closed_pred( + &mut self, + pred: Pred<'db>, + span: Option>, + ) -> Option> { + if !pred_is_closed(self.db, pred) { + return None; + } + let Some(trait_env) = self.try_module_trait_env(self.module) else { + self.push_missing_module_trait_env(span); + return None; + }; + match solve(self.db, trait_env, canonical_goal(self.db, pred)) { + Solution::Unique { evidence, .. } => Some(evidence), + Solution::Ambiguous { .. } | Solution::NoSolution => None, + } + } + + fn solve_reachable_pred( + &mut self, + pred: Pred<'db>, + span: Option>, + ) -> Option> { + if !pred_is_closed(self.db, pred) { + return None; + } + let mut found = None; + for module in self.modules.clone() { + let Some(trait_env) = self.try_module_trait_env(module) else { + self.push_missing_module_trait_env(span); + continue; + }; + let Solution::Unique { evidence, .. } = + solve(self.db, trait_env, canonical_goal(self.db, pred)) + else { + continue; + }; + if found.as_ref().is_some_and(|existing| existing != &evidence) { + return None; + } + found = Some(evidence); + } + found + } + + pub(super) fn solve_class_method_pred( + &mut self, + class: DefId<'db>, + method: &str, + callee_ty: Ty<'db>, + span: Option>, + ) -> Option> { + let info = self.classes.get(&class)?.clone(); + let method_sig = info + .class + .methods(self.db) + .iter() + .find(|candidate| ident_text(self.db, &candidate.name) == method)?; + let Some(resolution) = self.try_module_resolution(info.module) else { + self.push_missing_module_resolution(span); + return None; + }; + let method_type_vars = hir_ty::class_method_type_vars(self.db, info.class, method_sig); + let lowerer = TypeLowering::from_item_resolutions( + self.db, + &resolution.item_resolutions, + BinderEnv::from_type_vars(&method_type_vars), + ); + let mut normalizer = + AliasNormalizer::new(self.db, info.module, &resolution.item_resolutions); + let scheme = + normalizer.normalize_scheme(lowerer.lower_class_method(info.class, method_sig)); + let mut subst = TySubst::default(); + if !subst.match_ty(self.db, scheme.body(self.db).ty(self.db), callee_ty) { + return None; + } + let pred = scheme + .body(self.db) + .preds(self.db) + .iter() + .map(|pred| subst.apply_pred(self.db, *pred)) + .find(|pred| { + matches!( + pred.kind(self.db), + PredKind::InClass { + class: ClassId::User(def), + .. + } if *def == class + ) + })?; + self.solve_closed_pred(pred, span) + .or_else(|| self.solve_reachable_pred(pred, span)) + .or_else(|| self.derived_generic_evidence(pred)) + } + + pub(super) fn solve_operator_method_pred( + &mut self, + class_name: &str, + method: &str, + callee_ty: Ty<'db>, + span: Option>, + ) -> Option> { + let classes = self + .classes + .iter() + .filter_map(|(def, info)| { + (ident_text(self.db, &info.class.head(self.db).kind(self.db).class) == class_name) + .then_some(*def) + }) + .collect::>(); + let mut found = None; + for class in classes { + let Some(evidence) = self.solve_class_method_pred(class, method, callee_ty, span) + else { + continue; + }; + if found.as_ref().is_some_and(|existing| existing != &evidence) { + return None; + } + found = Some(evidence); + } + found + } + + pub(super) fn resolve_mptc_from_preds( + &self, + _module: Module<'db>, + preds: &[Pred<'db>], + subst: &mut TySubst<'db>, + ) { + for pred in preds { + let PredKind::InClass { class, main, args } = pred.kind(self.db) else { + continue; + }; + let main = subst.apply_ty(self.db, *main); + let extras = args + .iter() + .map(|arg| subst.apply_ty(self.db, *arg)) + .collect::>(); + if ty_is_closed(self.db, main) + && extras.iter().any(|extra| !ty_is_closed(self.db, *extra)) + { + self.try_resolve_mptc(*class, main, &extras, subst); + } + } + } + + fn try_resolve_mptc( + &self, + class: ClassId<'db>, + main: Ty<'db>, + extras: &[Ty<'db>], + subst: &mut TySubst<'db>, + ) { + let mut resolved = false; + for info in self.instances.values() { + let PredKind::InClass { + class: inst_class, + main: inst_main, + args: inst_args, + } = info.head.kind(self.db) + else { + continue; + }; + if *inst_class != class || inst_args.len() != extras.len() { + continue; + } + let mut phi = TySubst::default(); + if !phi.match_ty(self.db, *inst_main, main) { + continue; + } + let mut phi_with_eq = phi.clone(); + for pred in &info.preds { + if let PredKind::Eq { lhs, rhs } = phi.apply_pred(self.db, *pred).kind(self.db) { + match (lhs.kind(self.db), rhs.kind(self.db)) { + (TyKind::BoundVar(var), _) if ty_is_closed(self.db, *rhs) => { + phi_with_eq.insert_if_consistent(var.index, *rhs); + } + (_, TyKind::BoundVar(var)) if ty_is_closed(self.db, *lhs) => { + phi_with_eq.insert_if_consistent(var.index, *lhs); + } + _ => {} + } + } + } + let concrete_extras = inst_args + .iter() + .map(|arg| phi_with_eq.apply_ty(self.db, *arg)) + .collect::>(); + if !concrete_extras + .iter() + .all(|extra| ty_is_closed(self.db, *extra)) + { + continue; + } + for (extra, concrete) in extras.iter().zip(concrete_extras) { + let mut recovered = TySubst::default(); + if recovered.match_ty(self.db, *extra, concrete) { + subst.extend_consistent(recovered); + resolved = true; + } + } + } + if !resolved { + self.try_resolve_derived_generic_mptc(class, main, extras, subst); + } + } + + fn try_resolve_derived_generic_mptc( + &self, + class: ClassId<'db>, + main: Ty<'db>, + extras: &[Ty<'db>], + subst: &mut TySubst<'db>, + ) { + let ClassId::User(class_def) = class else { + return; + }; + if class_def.name(self.db).as_deref() != Some("Generic") || extras.len() != 1 { + return; + } + let TyKind::Named { + ctor: + TyCtor::User(UserTyCtor { + def, + kind: UserTyCtorKind::Adt, + }), + args, + } = main.kind(self.db) + else { + return; + }; + let Some(info) = self.adts.get(def) else { + return; + }; + let Some(plan) = derived_generic_instance_plan(self.db, info.module, info.adt, class_def) + else { + return; + }; + let concrete_rep = TySubst::from_args(args.clone()).apply_ty(self.db, plan.rep); + if !ty_is_closed(self.db, concrete_rep) { + return; + } + let mut recovered = TySubst::default(); + if recovered.match_ty(self.db, extras[0], concrete_rep) { + subst.extend_consistent(recovered); + } + } +} diff --git a/crates/specialize/src/specialize/intrinsics.rs b/crates/specialize/src/specialize/intrinsics.rs new file mode 100644 index 00000000..27f8a1f5 --- /dev/null +++ b/crates/specialize/src/specialize/intrinsics.rs @@ -0,0 +1,97 @@ +use super::*; + +pub(super) fn builtin_ctor_name(ctor: hir_nameres::BuiltinCtor) -> &'static str { + match ctor { + hir_nameres::BuiltinCtor::True => MonoBuiltinCtor::True.name(), + hir_nameres::BuiltinCtor::False => MonoBuiltinCtor::False.name(), + hir_nameres::BuiltinCtor::Unit => MonoBuiltinCtor::Unit.name(), + hir_nameres::BuiltinCtor::Pair => MonoBuiltinCtor::Pair.name(), + hir_nameres::BuiltinCtor::Inl => MonoBuiltinCtor::Inl.name(), + hir_nameres::BuiltinCtor::Inr => MonoBuiltinCtor::Inr.name(), + } +} + +pub(super) fn builtin_name(kind: hir_nameres::BuiltinKind) -> &'static str { + match kind { + hir_nameres::BuiltinKind::Constructor(ctor) => builtin_ctor_name(ctor), + hir_nameres::BuiltinKind::Function(function) => match function { + hir_nameres::BuiltinFunction::Invoke => "invoke", + hir_nameres::BuiltinFunction::PrimAddWord => "primAddWord", + hir_nameres::BuiltinFunction::PrimEqWord => "primEqWord", + hir_nameres::BuiltinFunction::WordToInteger => "wordToInteger", + hir_nameres::BuiltinFunction::WordFromInteger => "wordFromInteger", + hir_nameres::BuiltinFunction::IntegerAdd => "integerAdd", + hir_nameres::BuiltinFunction::IntegerSub => "integerSub", + hir_nameres::BuiltinFunction::IntegerMul => "integerMul", + hir_nameres::BuiltinFunction::IntegerLt => "integerLt", + hir_nameres::BuiltinFunction::IntegerEq => "integerEq", + }, + hir_nameres::BuiltinKind::ClassMethod(method) => match method { + hir_nameres::BuiltinClassMethod::InvokableInvoke => "invokable.invoke", + hir_nameres::BuiltinClassMethod::IntFromInteger => "Int.fromInteger", + }, + hir_nameres::BuiltinKind::Type(_) | hir_nameres::BuiltinKind::Class(_) => "", + } +} + +pub(super) fn overloaded_operator_method(op: BinOp) -> Option<(&'static str, &'static str)> { + match op { + BinOp::Add => Some(("Add", "add")), + BinOp::Sub => Some(("Sub", "sub")), + BinOp::Mul => Some(("Mul", "mul")), + BinOp::Div => Some(("Div", "div")), + BinOp::Mod => Some(("Mod", "mod")), + BinOp::BitAnd => Some(("BitAnd", "band")), + BinOp::BitXor => Some(("BitXor", "bxor")), + BinOp::BitOr => Some(("BitOr", "bor")), + BinOp::Eq => Some(("Eq", "eq")), + BinOp::Gt => Some(("Ord", "gt")), + BinOp::Pow => Some(("Pow", "pow")), + _ => None, + } +} + +pub(super) fn plain_operator_function(op: BinOp) -> Option<&'static str> { + match op { + BinOp::Lt => Some("lt"), + BinOp::NotEq => Some("ne"), + BinOp::LtEq => Some("le"), + BinOp::GtEq => Some("ge"), + BinOp::Shl => Some("bshlWord"), + BinOp::Shr => Some("bshrWord"), + _ => None, + } +} + +pub(super) fn builtin_intrinsic(kind: hir_nameres::BuiltinKind) -> Option { + match kind { + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimAddWord) => { + Some(MonoIntrinsic::PrimAddWord) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::PrimEqWord) => { + Some(MonoIntrinsic::PrimEqWord) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordToInteger) => { + Some(MonoIntrinsic::WordToInteger) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::WordFromInteger) => { + Some(MonoIntrinsic::WordFromInteger) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerAdd) => { + Some(MonoIntrinsic::IntegerAdd) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerSub) => { + Some(MonoIntrinsic::IntegerSub) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerMul) => { + Some(MonoIntrinsic::IntegerMul) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerLt) => { + Some(MonoIntrinsic::IntegerLt) + } + hir_nameres::BuiltinKind::Function(hir_nameres::BuiltinFunction::IntegerEq) => { + Some(MonoIntrinsic::IntegerEq) + } + _ => None, + } +} diff --git a/crates/specialize/src/specialize/mod.rs b/crates/specialize/src/specialize/mod.rs new file mode 100644 index 00000000..6b1417cd --- /dev/null +++ b/crates/specialize/src/specialize/mod.rs @@ -0,0 +1,143 @@ +use std::{ + collections::{VecDeque, hash_map::DefaultHasher}, + fmt, + hash::{Hash, Hasher}, + sync::Arc, +}; + +use hir::{ + Db as HirDb, + anchor::DefId, + arena::Id, + ast::{ + Ident, + function::{ + BinOp, Expr, ExprKind, FuncBody, FuncParam, MatchArm, Pat, PatKind, Stmt, StmtKind, + UnOp, + }, + item::{ + AdtDef, ContractItem, ContractKind, FuncKind, FunctionDef, Import, ImportSelector, + InstanceDef, Item, Module, + }, + }, + diag::{Diagnostic, DiagnosticCode}, + input::SourceFile, + nameres as hir_nameres, + span::{Span, Spanned, SpannedElem}, +}; +use hir_ty::infer::ComptimeObligation; +use hir_ty::{ + AbiParam, AliasNormalizer, BinderEnv, BodyDesugarView, BodyPreTypeckDesugarPlan, BodyTyContext, + BuiltinClassId, BuiltinTyCtor, CallSiteCallee, CallSiteEvidence, CheckedConversion, ClassId, + ComptimeObligationKind, Db, DispatchConstructor, DispatchFallback, Evidence, + GeneratedOriginKind, InferenceResult, LoweredFunction, Pred, PredKind, PreparedModule, + ProductShape, Solution, Ty, TyCtor, TyKind, TypeLowering, UserTyCtor, UserTyCtorKind, + canonical_goal, contract_dispatch_surface_for_module, contract_overlay_backend_name, + derived_generic_instance_plan, derived_generic_plan, frontend_desugar_plan, infer_body, + is_contract_deployment_main_def, is_contract_dispatch_main_def, + lower_normalized_function_with_inferred_signature, prepare_module, solve, + solver::DerivedClauseKind, trait_env_from_module_resolution, + trait_env_from_module_resolution_and_imports, trait_env_with_givens, +}; +use nameres::{LibraryId, ModuleId, module_key_for_path, resolve_reachable_full}; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + evaluate::{EvaluateOptions, evaluate_module}, + ir::{ + LetMode, MonoAbiParam, MonoArm, MonoBuiltinCtor, MonoCallOrigin, MonoComptimeObligation, + MonoComptimeObligationKind, MonoConstructor, MonoContract, MonoEntry, MonoExpr, + MonoExprArm, MonoExprKind, MonoFallback, MonoFunction, MonoFunctionOrigin, MonoId, + MonoIntrinsic, MonoItem, MonoModule, MonoParam, MonoPat, MonoPatKind, + MonoRuntimeMainOrigin, MonoStmt, MonoStmtKind, MonoTy, ParamMode, + }, +}; + +mod body; +mod call_resolver; +mod derived_generic; +mod diagnostics; +mod driver; +mod evidence; +mod intrinsics; +mod naming; +mod products; +mod ty_subst; + +use body::{BinOpExpr, BodyCtx, BodyIndex}; +pub use diagnostics::{SpecializeDiagnostic, SpecializeDiagnosticKind}; +use driver::{Driver, FunctionInfo, SpecKey, SyntheticKey}; +use intrinsics::{ + builtin_ctor_name, builtin_intrinsic, builtin_name, overloaded_operator_method, + plain_operator_function, +}; +pub(crate) use naming::display_backend_ty; +pub use naming::specialize_name; +use naming::{ + body_map_contains, class_method_name_parts, collect_body_order, ctor_name, def_hash_suffix, + def_owner_path, function_param_ty, function_ret_ty, ident_text, join_sanitized_name_components, + module_id_for_source_file, mono_abi_params, param_comptime, param_name, param_names, + pred_is_closed, reachable_modules, resolve_specialize_module, specialization_trait_env, + strip_comptime_ty, ty_is_builtin, ty_is_closed, ty_is_comptime, ty_node_budget_exceeded, + type_var_bindings, +}; +use products::{ + product_expr_from_elems, product_expr_from_vars, product_pat_from_elems, product_pat_from_vars, + product_vars, unwrap_sum_pat, var_expr, var_pattern, wrap_sum_expr, +}; +use ty_subst::TySubst; + +/// Specialization resource limits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpecializeOptions { + pub max_instantiations: usize, + /// Maximum nesting for specialization and evaluator inlining. + pub max_depth: usize, + pub max_type_nodes: usize, + pub eval_fuel: usize, +} + +impl Default for SpecializeOptions { + fn default() -> Self { + Self { + max_instantiations: 2048, + max_depth: 128, + max_type_nodes: 4096, + // This is a per-emitted-function work budget. It must accommodate + // the canonical std dispatch pipeline while still bounding + // exponential pure-call fan-out. + eval_fuel: 4096, + } + } +} + +/// Monomorphization output plus diagnostics. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SpecializeOutput<'db> { + pub module: MonoModule<'db>, + pub diagnostics: Vec>, +} + +/// Prepares and specializes one source HIR module from its backend entry surface. +/// +/// Call [`specialize_prepared_module`] when the caller already owns a +/// [`PreparedModule`]; passing only its effective `Module` here discards the +/// source constructor metadata kept by that wrapper. +pub fn specialize_module<'db>( + db: &'db dyn Db, + module: Module<'db>, + options: SpecializeOptions, +) -> SpecializeOutput<'db> { + let prepared = prepare_module(db, module); + specialize_prepared_module(db, prepared, options) +} + +/// Specializes an existing frontend overlay without preparing it a second time. +pub fn specialize_prepared_module<'db>( + db: &'db dyn Db, + prepared: PreparedModule<'db>, + options: SpecializeOptions, +) -> SpecializeOutput<'db> { + Driver::new(db, prepared, options).run() +} diff --git a/crates/specialize/src/specialize/naming.rs b/crates/specialize/src/specialize/naming.rs new file mode 100644 index 00000000..9cf7dc61 --- /dev/null +++ b/crates/specialize/src/specialize/naming.rs @@ -0,0 +1,547 @@ +pub(super) use hir::nameres::{ident_text, type_var_bindings}; + +use super::*; + +/// Reference-style specialization name: `base$word` or +/// `base$FooLword_boolJ`. +pub fn specialize_name<'db>(db: &'db dyn Db, base: &str, tys: &[Ty<'db>]) -> String { + let mut mangler = NameMangler::new(); + mangler.push_flattened_component(base); + if !tys.is_empty() { + mangler.push_raw("$"); + mangler.push_ty_list(db, tys); + } + mangler.finish() +} + +pub(super) fn param_name<'db>(db: &'db dyn HirDb, param: &FuncParam<'db>) -> Option<&'db str> { + match param { + FuncParam::Typed { name, .. } | FuncParam::Untyped { name, .. } => { + Some((*name.atom()).text(db)) + } + FuncParam::Error { .. } => None, + } +} + +pub(super) fn param_names<'db>(db: &'db dyn HirDb, params: &[FuncParam<'db>]) -> Vec { + params + .iter() + .map(|param| param_name(db, param).unwrap_or("_").to_owned()) + .collect() +} + +pub(crate) fn display_backend_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> String { + match ty.kind(db) { + TyKind::Error => "".to_owned(), + TyKind::Unknown | TyKind::BoundVar(_) => "_".to_owned(), + TyKind::Named { ctor, args } => { + if let TyCtor::Builtin(BuiltinTyCtor::FixedArray(length)) = ctor + && let [element] = args.as_slice() + { + return format!("{}[{length}]", display_backend_ty(db, *element)); + } + let name = match ctor { + TyCtor::Builtin(ctor) => ctor.name().to_owned(), + TyCtor::User(user) => user.def.name(db).unwrap_or_else(|| user.kind.to_string()), + }; + if args.is_empty() { + name + } else { + format!( + "{name}({})", + args.iter() + .map(|arg| display_backend_ty(db, *arg)) + .collect::>() + .join(", ") + ) + } + } + TyKind::Function { params, ret } => { + let params = params + .iter() + .map(|param| display_backend_ty(db, *param)) + .collect::>() + .join(", "); + format!("({params}) -> {}", display_backend_ty(db, *ret)) + } + TyKind::Tuple(elems) if elems.is_empty() => "()".to_owned(), + TyKind::Tuple(elems) => format!( + "({})", + elems + .iter() + .map(|elem| display_backend_ty(db, *elem)) + .collect::>() + .join(", ") + ), + TyKind::Comptime(inner) => format!("comptime {}", display_backend_ty(db, *inner)), + } +} + +pub(super) fn param_comptime(param: &FuncParam<'_>) -> bool { + match param { + FuncParam::Typed { comptime, .. } | FuncParam::Untyped { comptime, .. } => { + comptime.is_some() + } + FuncParam::Error { .. } => false, + } +} + +pub(super) fn body_map_contains<'db>( + map: &hir_nameres::BodyResolutionMap<'db>, + body: FuncBody<'db>, +) -> bool { + map.exprs.iter().any(|entry| entry.body == body) + || map.pats.iter().any(|entry| entry.body == body) + || map.stmt_bindings.iter().any(|entry| entry.body == body) +} + +pub(super) fn collect_body_order<'db>( + db: &'db dyn HirDb, + item: Item<'db>, + bodies: &mut Vec>, +) { + match item { + Item::FunctionDef(function) => { + if let Some(body) = function.body(db) { + bodies.push(body); + } + } + Item::InstanceDef(instance) => { + for method in instance.methods(db) { + if let Some(body) = method.body(db) { + bodies.push(body); + } + } + } + Item::ContractDef(contract) => { + for item in contract.items(db) { + if let ContractItem::FunctionDef(function) = *item + && let Some(body) = function.body(db) + { + bodies.push(body); + } + } + } + Item::TypeAlias(_) + | Item::AdtDef(_) + | Item::ClassDef(_) + | Item::Import(_) + | Item::Export(_) + | Item::Pragma(_) + | Item::Error { .. } => {} + } +} + +pub(super) fn reachable_modules<'db>(db: &'db dyn Db, entry: Module<'db>) -> Vec> { + let Some(entry_id) = module_id_for_source_file(db, entry.def_id_value(db).file(db)) else { + return vec![entry]; + }; + let graph = resolve_reachable_full(db, entry_id); + let mut modules = vec![entry]; + modules.extend( + graph + .modules + .into_iter() + .filter(|module| *module != entry_id) + .filter_map(|module| { + db.module_file(module).map(|file| { + let source = parse_file_to_hir(db, file).module(db); + prepare_module(db, source).module(db) + }) + }), + ); + modules +} + +pub(super) fn specialization_trait_env<'db>( + db: &'db dyn Db, + module: Module<'db>, + resolution: &hir_nameres::ModuleResolutionMap<'db>, +) -> hir_ty::TraitEnvId<'db> { + if let Some(module_id) = module_id_for_source_file(db, module.def_id_value(db).file(db)) { + let env = nameres::module_env_for_hir_module(db, module_id, module); + return trait_env_from_module_resolution_and_imports( + db, + module, + resolution, + &env.import_surface(), + ); + } + trait_env_from_module_resolution(db, module, resolution) +} + +pub(super) fn module_id_for_source_file<'db>( + db: &'db dyn Db, + file: SourceFile, +) -> Option> { + nameres::module_id_for_source_file(db, file) +} + +pub(super) fn resolve_specialize_module<'db>( + db: &'db dyn Db, + module: Module<'db>, +) -> hir_nameres::ModuleResolutionMap<'db> { + let Some(module_id) = module_id_for_source_file(db, module.def_id_value(db).file(db)) else { + return hir_nameres::resolve_module(db, module); + }; + let env = nameres::module_env_for_hir_module(db, module_id, module); + let Some(item_scope) = env.item_scope.clone() else { + return hir_nameres::resolve_module(db, module); + }; + hir_nameres::resolve_module_with_imports_and_policy( + db, + module, + item_scope, + &env, + hir_nameres::NameresDiagnosticPolicy::Emit, + ) +} + +pub(super) fn mono_abi_params(params: Vec) -> Vec { + params + .into_iter() + .map(|param| MonoAbiParam { + name: param.name, + ty: param.ty, + components: mono_abi_params(param.components), + }) + .collect() +} + +pub(super) fn function_param_ty<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + index: usize, +) -> Option> { + match ty.kind(db) { + TyKind::Function { params, .. } => params.get(index).copied(), + TyKind::Comptime(inner) => function_param_ty(db, *inner, index), + _ => None, + } +} + +pub(super) fn function_ret_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Option> { + match ty.kind(db) { + TyKind::Function { ret, .. } => Some(*ret), + TyKind::Comptime(inner) => function_ret_ty(db, *inner), + _ => None, + } +} + +pub(super) fn def_owner_path<'db>(db: &'db dyn HirDb, def: DefId<'db>) -> Vec { + let mut out = Vec::new(); + let mut owner = def.owner(db); + while let Some(current) = owner { + if let Some(name) = current.name(db) { + out.push(name); + } else if current.owner(db).is_none() { + out.push(source_file_stem(current.file(db).url(db).path())); + } + owner = current.owner(db); + } + out.reverse(); + if out.is_empty() { + out.push(source_file_stem(def.file(db).url(db).path())); + } + out +} + +fn source_file_stem(path: &str) -> String { + let file = path.rsplit('/').next().unwrap_or(path); + file.rsplit_once('.') + .map(|(stem, _)| stem) + .unwrap_or(file) + .to_owned() +} + +pub(super) fn def_hash_suffix<'db>(db: &'db dyn Db, def: DefId<'db>) -> String { + let mut hasher = DefaultHasher::new(); + hash_def_id(db, def, &mut hasher); + format!("d{:08x}", (hasher.finish() & 0xffff_ffff) as u32) +} + +fn hash_def_id<'db>(db: &'db dyn Db, def: DefId<'db>, state: &mut DefaultHasher) { + hash_source_file_identity(db, def.file(db), state); + def.kind(db).hash(state); + contract_overlay_backend_name(db, def) + .map(str::to_owned) + .or_else(|| def.name(db)) + .hash(state); + def.fingerprint(db).hash(state); + def.disambiguator(db).as_u32().hash(state); + if let Some(owner) = def.owner(db) { + hash_def_id(db, owner, state); + } +} + +fn hash_source_file_identity(db: &dyn Db, file: SourceFile, state: &mut DefaultHasher) { + if let Some(module) = module_id_for_source_file(db, file) { + module.library(db).hash(state); + module.logical_path(db).hash(state); + } else { + file.url(db).as_str().hash(state); + } +} + +pub(super) fn join_sanitized_name_components( + components: impl IntoIterator, +) -> String { + let mut mangler = NameMangler::new(); + let mut first = true; + for component in components { + if component.is_empty() { + continue; + } + if !first { + mangler.push_raw("_"); + } + let component = sanitize_name_component(&component); + mangler.push_raw(&component); + first = false; + } + mangler.finish() +} + +pub(super) fn sanitize_name_component(component: &str) -> String { + let mut mangler = NameMangler::new(); + mangler.push_component(component); + mangler.finish() +} + +struct NameMangler { + out: String, +} + +impl NameMangler { + fn new() -> Self { + Self { out: String::new() } + } + + fn push_raw(&mut self, raw: &str) { + self.out.push_str(raw); + } + + fn push_component(&mut self, component: &str) { + self.push_component_with(component, ComponentPolicy::Identifier); + } + + fn push_flattened_component(&mut self, component: &str) { + self.push_component_with(component, ComponentPolicy::DottedPath); + } + + fn push_component_with(&mut self, component: &str, policy: ComponentPolicy) { + let start = self.out.len(); + for ch in component.chars() { + self.out.push(policy.sanitize(ch)); + } + if policy.empty_component_is_underscore() && self.out.len() == start { + self.out.push('_'); + } + } + + fn push_ty_list<'db>(&mut self, db: &'db dyn Db, tys: &[Ty<'db>]) { + for (index, ty) in tys.iter().enumerate() { + if index > 0 { + self.out.push('_'); + } + self.push_ty(db, *ty); + } + } + + fn push_ty<'db>(&mut self, db: &'db dyn Db, ty: Ty<'db>) { + match ty.kind(db) { + TyKind::Named { ctor, args } => { + let (name, user_def) = match ctor { + TyCtor::Builtin(ctor) => { + if *ctor == BuiltinTyCtor::Unit && args.is_empty() { + self.out.push_str("unit"); + return; + } + let name = match ctor { + BuiltinTyCtor::FixedArray(length) => { + format!("fixed_array_{length}") + } + _ => ctor.name().to_owned(), + }; + (name, None) + } + TyCtor::User(user) => ( + user.def + .name(db) + .unwrap_or_else(|| format!("{:?}", user.def.kind(db))), + Some(user.def), + ), + }; + self.push_flattened_component(&name); + if let Some(def) = user_def { + self.out.push('_'); + self.push_raw(&def_hash_suffix(db, def)); + } + if !args.is_empty() { + self.out.push('L'); + self.push_ty_list(db, args); + self.out.push('J'); + } + } + TyKind::Tuple(elems) if elems.is_empty() => self.out.push_str("unit"), + TyKind::Tuple(elems) => { + self.out.push_str("pairL"); + self.push_ty_list(db, elems); + self.out.push('J'); + } + TyKind::BoundVar(var) => { + self.out.push('t'); + self.out.push_str(&var.index.to_string()); + } + TyKind::Comptime(inner) => self.push_ty(db, *inner), + TyKind::Function { params, ret } => { + self.out.push_str("fnL"); + self.push_ty_list(db, params); + self.out.push_str("JR"); + self.push_ty(db, *ret); + self.out.push('J'); + } + TyKind::Error => self.out.push_str("error"), + TyKind::Unknown => self.out.push_str("unknown"), + } + } + + fn finish(self) -> String { + self.out + } +} + +#[derive(Clone, Copy)] +enum ComponentPolicy { + DottedPath, + Identifier, +} + +impl ComponentPolicy { + fn sanitize(self, ch: char) -> char { + match self { + ComponentPolicy::DottedPath if ch == '.' => '_', + ComponentPolicy::DottedPath => ch, + ComponentPolicy::Identifier if ch.is_ascii_alphanumeric() || ch == '_' => ch, + ComponentPolicy::Identifier => '_', + } + } + + fn empty_component_is_underscore(self) -> bool { + matches!(self, ComponentPolicy::Identifier) + } +} + +pub(super) fn ty_is_closed<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + match ty.kind(db) { + TyKind::Error => true, + TyKind::Unknown | TyKind::BoundVar(_) => false, + TyKind::Named { args, .. } => args.iter().all(|arg| ty_is_closed(db, *arg)), + TyKind::Function { params, ret } => { + params.iter().all(|param| ty_is_closed(db, *param)) && ty_is_closed(db, *ret) + } + TyKind::Tuple(elems) => elems.iter().all(|elem| ty_is_closed(db, *elem)), + TyKind::Comptime(inner) => ty_is_closed(db, *inner), + } +} + +pub(super) fn ty_node_budget_exceeded<'db>(db: &'db dyn Db, ty: Ty<'db>, limit: usize) -> bool { + let mut remaining = limit; + !consume_ty_node_budget(db, ty, &mut remaining) +} + +fn consume_ty_node_budget<'db>(db: &'db dyn Db, ty: Ty<'db>, remaining: &mut usize) -> bool { + if *remaining == 0 { + return false; + } + *remaining -= 1; + match ty.kind(db) { + TyKind::Named { args, .. } => args + .iter() + .all(|arg| consume_ty_node_budget(db, *arg, remaining)), + TyKind::Function { params, ret } => { + params + .iter() + .all(|param| consume_ty_node_budget(db, *param, remaining)) + && consume_ty_node_budget(db, *ret, remaining) + } + TyKind::Tuple(elems) => elems + .iter() + .all(|elem| consume_ty_node_budget(db, *elem, remaining)), + TyKind::Comptime(inner) => consume_ty_node_budget(db, *inner, remaining), + TyKind::Error | TyKind::Unknown | TyKind::BoundVar(_) => true, + } +} + +pub(super) fn pred_is_closed<'db>(db: &'db dyn Db, pred: Pred<'db>) -> bool { + match pred.kind(db) { + PredKind::InClass { main, args, .. } => { + ty_is_closed(db, *main) && args.iter().all(|arg| ty_is_closed(db, *arg)) + } + PredKind::Eq { lhs, rhs } => ty_is_closed(db, *lhs) && ty_is_closed(db, *rhs), + PredKind::Error => true, + } +} + +pub(super) fn ty_is_builtin<'db>(db: &'db dyn Db, ty: Ty<'db>, builtin: BuiltinTyCtor) -> bool { + matches!( + strip_comptime_ty(db, ty).kind(db), + TyKind::Named { + ctor: TyCtor::Builtin(ctor), + args, + } if *ctor == builtin && args.is_empty() + ) +} + +pub(super) fn ty_is_comptime<'db>(db: &'db dyn Db, ty: Ty<'db>) -> bool { + matches!(ty.kind(db), TyKind::Comptime(_)) +} + +pub(super) fn strip_comptime_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(db) { + TyKind::Comptime(inner) => strip_comptime_ty(db, *inner), + _ => ty, + } +} + +pub(super) fn class_method_name_parts<'db>( + db: &'db dyn HirDb, + pred: Pred<'db>, +) -> (String, Vec>) { + match pred.kind(db) { + PredKind::InClass { + class, main, args, .. + } => { + let class = match class { + ClassId::Builtin(class) => class.name().to_owned(), + ClassId::User(def) => def.name(db).unwrap_or_else(|| "Class".to_owned()), + }; + let mut head_tys = Vec::with_capacity(args.len() + 1); + head_tys.push(*main); + head_tys.extend(args.iter().copied()); + (class, head_tys) + } + _ => ("Class".to_owned(), Vec::new()), + } +} + +pub(super) fn ctor_name<'db>( + db: &'db dyn HirDb, + adt: Option>, + index: hir_nameres::CtorIndex, +) -> String { + let raw_index = index.as_u32(); + let Some(adt) = adt else { + return format!("ctor{raw_index}"); + }; + let ty = adt + .def_id_value(db) + .name(db) + .unwrap_or_else(|| "Adt".to_owned()); + let ctor = adt + .ctors(db) + .get(index.as_usize()) + .map(|ctor| ident_text(db, &ctor.name)) + .unwrap_or_else(|| format!("ctor{raw_index}")); + format!("{ty}_{ctor}") +} diff --git a/crates/specialize/src/specialize/products.rs b/crates/specialize/src/specialize/products.rs new file mode 100644 index 00000000..d38d68eb --- /dev/null +++ b/crates/specialize/src/specialize/products.rs @@ -0,0 +1,327 @@ +use super::*; + +pub(super) struct ProductVar<'db> { + id: MonoId<'db>, +} + +pub(super) fn product_vars<'db>( + db: &'db dyn Db, + ty: Ty<'db>, + arity: usize, + span: Span<'db>, + prefix: &str, +) -> Option>> { + product_fields_exact(db, ty, arity)? + .into_iter() + .enumerate() + .map(|(index, ty)| ProductVar { + id: MonoId { + name: format!("{prefix}{index}"), + ty: MonoTy::new_unchecked(ty), + span, + }, + }) + .collect::>() + .into() +} + +fn product_fields_exact<'db>( + db: &'db dyn Db, + mut product: Ty<'db>, + arity: usize, +) -> Option>> { + if arity == 0 { + return ty_is_builtin(db, product, BuiltinTyCtor::Unit).then(Vec::new); + } + let mut fields = Vec::with_capacity(arity); + for _ in 1..arity { + let TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } = product.kind(db) + else { + return None; + }; + if args.len() != 2 { + return None; + } + fields.push(args[0]); + product = args[1]; + } + fields.push(product); + Some(fields) +} + +pub(super) fn var_expr<'db>(var: &ProductVar<'db>, span: Span<'db>) -> MonoExpr<'db> { + MonoExpr { + span, + ty: var.id.ty, + kind: MonoExprKind::Var(var.id.clone()), + } +} + +pub(super) fn var_pattern<'db>(var: &ProductVar<'db>, span: Span<'db>) -> MonoPat<'db> { + MonoPat { + span, + ty: var.id.ty, + kind: MonoPatKind::Var(var.id.clone()), + } +} + +pub(super) fn product_expr_from_vars<'db>( + db: &'db dyn Db, + vars: &[ProductVar<'db>], + ty: Ty<'db>, + span: Span<'db>, +) -> MonoExpr<'db> { + match vars { + [] => MonoExpr { + span, + ty: MonoTy::new_unchecked(Ty::unit(db)), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Unit.name().to_owned(), + ty: MonoTy::new_unchecked(Ty::unit(db)), + span, + }, + args: Vec::new(), + }, + }, + [one] => var_expr(one, span), + [head, tail @ ..] => MonoExpr { + span, + ty: MonoTy::new_unchecked(ty), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Pair.name().to_owned(), + ty: MonoTy::new_unchecked(ty), + span, + }, + args: vec![ + var_expr(head, span), + product_expr_from_vars(db, tail, pair_tail_ty(db, ty), span), + ], + }, + }, + } +} + +pub(super) fn product_expr_from_elems<'db>( + db: &'db dyn Db, + elems: &[MonoExpr<'db>], + ty: Ty<'db>, + span: Span<'db>, +) -> MonoExpr<'db> { + match elems { + [] => MonoExpr { + span, + ty: MonoTy::new_unchecked(Ty::unit(db)), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Unit.name().to_owned(), + ty: MonoTy::new_unchecked(Ty::unit(db)), + span, + }, + args: Vec::new(), + }, + }, + [one] => { + let mut expr = one.clone(); + expr.span = span; + expr.ty = MonoTy::new_unchecked(ty); + expr + } + [head, tail @ ..] => MonoExpr { + span, + ty: MonoTy::new_unchecked(ty), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Pair.name().to_owned(), + ty: MonoTy::new_unchecked(ty), + span, + }, + args: vec![ + head.clone(), + product_expr_from_elems(db, tail, pair_tail_ty(db, ty), span), + ], + }, + }, + } +} + +pub(super) fn product_pat_from_vars<'db>( + db: &'db dyn Db, + vars: &[ProductVar<'db>], + ty: Ty<'db>, + span: Span<'db>, +) -> MonoPat<'db> { + match vars { + [] => MonoPat { + span, + ty: MonoTy::new_unchecked(Ty::unit(db)), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Unit.name().to_owned(), + ty: MonoTy::new_unchecked(Ty::unit(db)), + span, + }, + args: Vec::new(), + }, + }, + [one] => var_pattern(one, span), + [head, tail @ ..] => MonoPat { + span, + ty: MonoTy::new_unchecked(ty), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Pair.name().to_owned(), + ty: MonoTy::new_unchecked(ty), + span, + }, + args: vec![ + var_pattern(head, span), + product_pat_from_vars(db, tail, pair_tail_ty(db, ty), span), + ], + }, + }, + } +} + +pub(super) fn product_pat_from_elems<'db>( + db: &'db dyn Db, + elems: &[MonoPat<'db>], + ty: Ty<'db>, + span: Span<'db>, +) -> MonoPat<'db> { + match elems { + [] => MonoPat { + span, + ty: MonoTy::new_unchecked(Ty::unit(db)), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Unit.name().to_owned(), + ty: MonoTy::new_unchecked(Ty::unit(db)), + span, + }, + args: Vec::new(), + }, + }, + [one] => { + let mut pat = one.clone(); + pat.span = span; + pat.ty = MonoTy::new_unchecked(ty); + pat + } + [head, tail @ ..] => MonoPat { + span, + ty: MonoTy::new_unchecked(ty), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Pair.name().to_owned(), + ty: MonoTy::new_unchecked(ty), + span, + }, + args: vec![ + head.clone(), + product_pat_from_elems(db, tail, pair_tail_ty(db, ty), span), + ], + }, + }, + } +} + +fn pair_tail_ty<'db>(db: &'db dyn Db, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(db) { + TyKind::Named { + ctor: TyCtor::Builtin(BuiltinTyCtor::Pair), + args, + } if args.len() == 2 => args[1], + _ => Ty::unit(db), + } +} + +pub(super) fn wrap_sum_expr<'db>( + db: &'db dyn Db, + mut expr: MonoExpr<'db>, + rep: Ty<'db>, + inr_depth: u32, + wraps_inl: bool, + span: Span<'db>, +) -> MonoExpr<'db> { + if wraps_inl { + expr = MonoExpr { + span, + ty: MonoTy::new_unchecked(rep), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Inl.name().to_owned(), + ty: MonoTy::new_unchecked(rep), + span, + }, + args: vec![expr], + }, + }; + } + for _ in 0..inr_depth { + expr = MonoExpr { + span, + ty: MonoTy::new_unchecked(rep), + kind: MonoExprKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Inr.name().to_owned(), + ty: MonoTy::new_unchecked(rep), + span, + }, + args: vec![expr], + }, + }; + } + if inr_depth == 0 && !wraps_inl { + expr.ty = MonoTy::new_unchecked(rep); + } + let _ = db; + expr +} + +pub(super) fn unwrap_sum_pat<'db>( + db: &'db dyn Db, + mut pat: MonoPat<'db>, + rep: Ty<'db>, + inr_depth: u32, + wraps_inl: bool, + span: Span<'db>, +) -> MonoPat<'db> { + if wraps_inl { + pat = MonoPat { + span, + ty: MonoTy::new_unchecked(rep), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Inl.name().to_owned(), + ty: MonoTy::new_unchecked(rep), + span, + }, + args: vec![pat], + }, + }; + } + for _ in 0..inr_depth { + pat = MonoPat { + span, + ty: MonoTy::new_unchecked(rep), + kind: MonoPatKind::Con { + ctor: MonoId { + name: MonoBuiltinCtor::Inr.name().to_owned(), + ty: MonoTy::new_unchecked(rep), + span, + }, + args: vec![pat], + }, + }; + } + if inr_depth == 0 && !wraps_inl { + pat.ty = MonoTy::new_unchecked(rep); + } + let _ = db; + pat +} diff --git a/crates/specialize/src/specialize/ty_subst.rs b/crates/specialize/src/specialize/ty_subst.rs new file mode 100644 index 00000000..41029a82 --- /dev/null +++ b/crates/specialize/src/specialize/ty_subst.rs @@ -0,0 +1,166 @@ +use super::*; + +#[derive(Debug, Clone, Default)] +pub(super) struct TySubst<'db> { + vars: FxHashMap>, +} + +impl<'db> TySubst<'db> { + pub(super) fn from_args(args: Vec>) -> Self { + let vars = args + .into_iter() + .enumerate() + .map(|(index, ty)| (index as u32, ty)) + .collect(); + Self { vars } + } + + pub(super) fn specialization_args(&self) -> Vec> { + let mut args = self.vars.iter().collect::>(); + args.sort_by_key(|(index, _)| **index); + args.into_iter().map(|(_, ty)| *ty).collect() + } + + pub(super) fn insert_if_consistent(&mut self, index: u32, ty: Ty<'db>) -> bool { + match self.vars.get(&index) { + Some(existing) if *existing != ty => false, + Some(_) => true, + None => { + self.vars.insert(index, ty); + true + } + } + } + + pub(super) fn extend_consistent(&mut self, other: TySubst<'db>) { + for (index, ty) in other.vars { + self.insert_if_consistent(index, ty); + } + } + + pub(super) fn match_ty(&mut self, db: &'db dyn Db, pattern: Ty<'db>, target: Ty<'db>) -> bool { + let pattern = strip_comptime_ty(db, pattern); + let target = strip_comptime_ty(db, target); + match pattern.kind(db) { + TyKind::BoundVar(var) => match self.vars.get(&var.index) { + Some(existing) => *existing == target, + None => { + self.vars.insert(var.index, target); + true + } + }, + TyKind::Named { ctor, args } => match target.kind(db) { + TyKind::Named { + ctor: target_ctor, + args: target_args, + } if ctor == target_ctor && args.len() == target_args.len() => args + .iter() + .zip(target_args) + .all(|(arg, target)| self.match_ty(db, *arg, *target)), + _ => false, + }, + TyKind::Function { params, ret } => match target.kind(db) { + TyKind::Function { + params: target_params, + ret: target_ret, + } if params.len() == target_params.len() => { + params + .iter() + .zip(target_params) + .all(|(param, target)| self.match_ty(db, *param, *target)) + && self.match_ty(db, *ret, *target_ret) + } + _ => false, + }, + TyKind::Tuple(elems) => match target.kind(db) { + TyKind::Tuple(target_elems) if elems.len() == target_elems.len() => elems + .iter() + .zip(target_elems) + .all(|(elem, target)| self.match_ty(db, *elem, *target)), + _ => false, + }, + TyKind::Comptime(inner) => match target.kind(db) { + TyKind::Comptime(target_inner) => self.match_ty(db, *inner, *target_inner), + _ => self.match_ty(db, *inner, target), + }, + TyKind::Error | TyKind::Unknown => true, + } + } + + pub(super) fn apply_ty(&self, db: &'db dyn Db, ty: Ty<'db>) -> Ty<'db> { + match ty.kind(db) { + TyKind::BoundVar(var) => self.vars.get(&var.index).copied().unwrap_or(ty), + TyKind::Named { ctor, args } => Ty::named( + db, + *ctor, + args.iter().map(|arg| self.apply_ty(db, *arg)).collect(), + ), + TyKind::Function { params, ret } => Ty::function( + db, + params + .iter() + .map(|param| self.apply_ty(db, *param)) + .collect(), + self.apply_ty(db, *ret), + ), + TyKind::Tuple(elems) => Ty::tuple( + db, + elems.iter().map(|elem| self.apply_ty(db, *elem)).collect(), + ), + TyKind::Comptime(inner) => Ty::comptime(db, self.apply_ty(db, *inner)), + TyKind::Error | TyKind::Unknown => ty, + } + } + + pub(super) fn apply_pred(&self, db: &'db dyn Db, pred: Pred<'db>) -> Pred<'db> { + match pred.kind(db) { + PredKind::InClass { class, main, args } => Pred::in_class( + db, + *class, + self.apply_ty(db, *main), + args.iter().map(|arg| self.apply_ty(db, *arg)).collect(), + ), + PredKind::Eq { lhs, rhs } => { + Pred::eq(db, self.apply_ty(db, *lhs), self.apply_ty(db, *rhs)) + } + PredKind::Error => pred, + } + } + + pub(super) fn apply_evidence(&self, db: &'db dyn Db, evidence: Evidence<'db>) -> Evidence<'db> { + match evidence { + Evidence::Instance { + instance, + args, + sub_evidence, + } => Evidence::Instance { + instance, + args: args.into_iter().map(|arg| self.apply_ty(db, arg)).collect(), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| self.apply_evidence(db, evidence)) + .collect(), + }, + Evidence::Builtin { pred } => Evidence::Builtin { + pred: self.apply_pred(db, pred), + }, + Evidence::Superclass { class, pred, child } => Evidence::Superclass { + class, + pred: self.apply_pred(db, pred), + child: Box::new(self.apply_evidence(db, *child)), + }, + Evidence::Derived { + kind, + pred, + sub_evidence, + } => Evidence::Derived { + kind, + pred: self.apply_pred(db, pred), + sub_evidence: sub_evidence + .into_iter() + .map(|evidence| self.apply_evidence(db, evidence)) + .collect(), + }, + } + } +} diff --git a/crates/specialize/tests/specialize.rs b/crates/specialize/tests/specialize.rs new file mode 100644 index 00000000..ccfb21d3 --- /dev/null +++ b/crates/specialize/tests/specialize.rs @@ -0,0 +1,3677 @@ +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + fs, + path::{Path, PathBuf}, +}; + +use hir::{ + anchor::DefLocationTable, + ast::{ + function::{YulExprKind, YulStmtKind}, + item::Module, + }, + input::SourceFile, + nameres::ident_text, +}; +use hir_ty::{BuiltinTyCtor, ConversionKind, Ty, prepare_module}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, module_key_for_path, module_path_display, resolve_module_path_candidate, +}; +use parser::parse_file_to_hir; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::Setter; +use solcore_specialize::{ + MonoComptimeObligationKind, MonoEntry, MonoExpr, MonoExprKind, MonoItem, MonoPatKind, + MonoRuntimeMainOrigin, MonoStmt, MonoStmtKind, SpecializeDiagnosticKind, SpecializeOptions, + SpecializeOutput, specialize_module, specialize_name, specialize_prepared_module, +}; + +#[salsa::db] +#[derive(Default, Clone)] +struct TestDb { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: FxHashMap, +} + +impl TestDb { + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile) { + if self.module_files.insert(key, file) == Some(file) { + return; + } + let files = self + .module_files + .iter() + .map(|(key, file)| (key.clone(), *file)) + .collect(); + if let Some(snapshot) = self.module_file_snapshot { + snapshot.set_files(self).to(files); + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } +} + +#[salsa::db] +impl salsa::Database for TestDb {} + +#[salsa::db] +impl hir::Db for TestDb { + fn def_location_table<'db>(&'db self, file: SourceFile) -> &'db DefLocationTable<'db> { + parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for TestDb {} + +#[salsa::db] +impl nameres::Db for TestDb { + fn module_tree(&self) -> ModuleTree { + self.module_tree.unwrap_or_else(|| { + ModuleTree::new( + self, + PathBuf::from("/main"), + PathBuf::from("/std"), + BTreeMap::new(), + ) + }) + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .unwrap_or_else(|| ModuleFsSnapshot::new(self, BTreeSet::new(), BTreeMap::new())) + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .unwrap_or_else(|| ModuleFileSnapshot::new(self, BTreeMap::new())) + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl hir_ty::Db for TestDb {} + +fn source_file(db: &TestDb, name: &str, src: &str) -> SourceFile { + let url = format!("memory:///{name}.solc").parse().expect("valid URL"); + SourceFile::new(db, url, Some(src.to_owned())) +} + +fn source_file_at_path(db: &TestDb, path: &Path, src: &str) -> SourceFile { + SourceFile::new( + db, + url::Url::from_file_path(path).expect("file URL"), + Some(src.to_owned()), + ) +} + +fn parse_module<'db>(db: &'db TestDb, src: &str) -> Module<'db> { + parse_file_to_hir(db, source_file(db, "test", src)).module(db) +} + +fn specialize_src(src: &str) -> (&'static TestDb, SpecializeOutput<'static>) { + let db = Box::leak(Box::new(TestDb::default())); + let module = parse_module(db, src); + let output = specialize_module(db, module, SpecializeOptions::default()); + (db, output) +} + +fn specialize_src_with_std(src: &str) -> SpecializeOutput<'static> { + specialize_src_with_std_and_db(src).2 +} + +fn specialize_src_with_std_and_db( + src: &str, +) -> (&'static TestDb, SourceFile, SpecializeOutput<'static>) { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + let repo = repo_root(); + let std_root = repo.join("crates/parser/tests/fixtures/corpus/ok/std"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + db, + [main_root.as_path(), std_root.as_path()], + )); + let main_path = main_root.join("main.solc"); + let key = + module_key_for_path(LibraryId::Main, &main_root, &main_path).expect("file under main root"); + let file = source_file_at_path(db, &main_path, src); + db.insert_module_file(key.clone(), file); + let unresolved = load_reachable_modules(db, key); + assert!(unresolved.is_empty(), "{unresolved:?}"); + let module = parse_file_to_hir(db, file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + (db, file, output) +} + +fn function_names(output: &SpecializeOutput<'_>) -> Vec { + let mut names = output + .module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => Some(function.name.clone()), + _ => None, + }) + .collect::>(); + names.sort(); + names +} + +#[test] +fn specializes_large_linear_body_with_indexed_frontend_lookups() { + use std::fmt::Write as _; + + let mut source = "function main() returns (word) {\n let value0: word = 0;\n".to_owned(); + for index in 1..2_000 { + writeln!( + &mut source, + " let value{index} : word = value{};", + index - 1 + ) + .unwrap(); + } + writeln!(&mut source, " return value1999;\n}}").unwrap(); + + let (_db, output) = specialize_src(&source); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + assert!(!function_names(&output).is_empty()); +} + +#[test] +fn interface_and_library_shells_are_not_deployment_roots() { + let (_, output) = specialize_src( + r#" +interface Reader { + function read(key: word) external view returns (word); +} + +library Helpers { + function main() internal returns (word) { return 99; } +} + +function main() returns (word) { return 1; } +"#, + ); + + assert!( + output + .module + .items + .iter() + .all(|item| !matches!(item, MonoItem::Contract(_))), + "{:#?}", + output.module.items + ); + assert!( + output.diagnostics.iter().all(|diagnostic| !matches!( + diagnostic.kind, + SpecializeDiagnosticKind::MissingBody { .. } + )), + "{:#?}", + output.diagnostics + ); + assert_eq!( + function_names(&output).len(), + 1, + "{:#?}", + output.module.items + ); +} + +#[test] +fn local_library_functions_are_reusable_without_becoming_contracts() { + let (_, output) = specialize_src( + r#" +library Helpers { + function id(x: word) internal returns (word) { return x; } +} + +function main() returns (word) { return Helpers.id(41); } +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:#?}", output.diagnostics); + assert!( + output + .module + .items + .iter() + .all(|item| !matches!(item, MonoItem::Contract(_))), + "{:#?}", + output.module.items + ); + assert_eq!( + function_names(&output).len(), + 1, + "{:#?}", + output.module.items + ); +} + +#[test] +fn imported_library_members_keep_their_qualified_surface() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + + let helper_path = main_root.join("helper.solc"); + let helper_file = source_file_at_path( + db, + &helper_path, + r#" +export { Helpers }; +library Helpers { + alias Value = word; + function id(x: Value) internal returns (Value) { return x; } +} +"#, + ); + let main_path = main_root.join("main.solc"); + let main_file = source_file_at_path( + db, + &main_path, + r#" +import {Helpers} from helper; +function main(x: Helpers.Value) returns (Helpers.Value) { + return Helpers.id(x); +} +"#, + ); + let helper_key = module_key_for_path(LibraryId::Main, &main_root, &helper_path).unwrap(); + let main_key = module_key_for_path(LibraryId::Main, &main_root, &main_path).unwrap(); + db.insert_module_file(helper_key, helper_file); + db.insert_module_file(main_key, main_file); + + let module = parse_file_to_hir(db, main_file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert!(output.diagnostics.is_empty(), "{:#?}", output.diagnostics); + assert_eq!( + function_names(&output).len(), + 2, + "{:#?}", + output.module.items + ); +} + +#[test] +fn imported_fixed_array_aliases_reach_runtime_type_rejection() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + + let helper_path = main_root.join("helper.solc"); + let helper_file = source_file_at_path( + db, + &helper_path, + r#" +export { Helpers }; +library Helpers { + alias Fixed = word[3]; +} +"#, + ); + let main_path = main_root.join("main.solc"); + let main_file = source_file_at_path( + db, + &main_path, + r#" +import {Helpers} from helper; +function main(x: Helpers.Fixed) returns (Helpers.Fixed) { return x; } +"#, + ); + let helper_key = module_key_for_path(LibraryId::Main, &main_root, &helper_path).unwrap(); + let main_key = module_key_for_path(LibraryId::Main, &main_root, &main_path).unwrap(); + db.insert_module_file(helper_key, helper_file); + db.insert_module_file(main_key, main_file); + + let module = parse_file_to_hir(db, main_file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + let diagnostic = output + .diagnostics + .iter() + .find(|diagnostic| matches!( + &diagnostic.kind, + SpecializeDiagnosticKind::UnsupportedRuntimeType { ty, .. } if ty.contains("word[3]") + )) + .expect("qualified fixed-array runtime diagnostic"); + assert_eq!(diagnostic.lower(db).code.as_deref(), Some("SC0416")); + assert!( + output.module.items.iter().all(|item| !matches!( + item, + MonoItem::Function(function) if function.name.contains("main") + )), + "{:#?}", + output.module.items + ); +} + +#[test] +fn no_contract_fallback_only_roots_main_from_entry_module() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + + let helper_path = main_root.join("helper.solc"); + let helper_file = source_file_at_path( + db, + &helper_path, + r#" +export { helper_value }; +function helper_value() returns (word) { return 7; } +function main() returns (word) { return 99; } +"#, + ); + let main_path = main_root.join("main.solc"); + let main_file = source_file_at_path( + db, + &main_path, + r#" +import {helper_value} from helper; +function main() returns (word) { return helper_value(); } +"#, + ); + let helper_key = module_key_for_path(LibraryId::Main, &main_root, &helper_path).unwrap(); + let main_key = module_key_for_path(LibraryId::Main, &main_root, &main_path).unwrap(); + db.insert_module_file(helper_key, helper_file); + db.insert_module_file(main_key, main_file); + + let module = parse_file_to_hir(db, main_file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert!(output.diagnostics.is_empty(), "{:#?}", output.diagnostics); + assert_eq!( + function_names(&output).len(), + 1, + "{:#?}", + output.module.items + ); +} + +#[test] +fn specialization_preserves_checked_identity_conversion_kind() { + let (_db, output) = specialize_src( + r#" +alias W = word; + +function main(x: word) returns (word) { + return x as W as word; +} +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains("main") => Some(function), + _ => None, + }) + .expect("specialized main function"); + let MonoStmtKind::Return(Some(ret)) = &main.body[0].kind else { + panic!("expected identity return: {:#?}", main.body); + }; + let MonoExprKind::Conversion { + expr: inner, + kind: ConversionKind::Identity, + .. + } = &ret.kind + else { + panic!("expected outer identity conversion: {ret:#?}"); + }; + assert!(matches!( + inner.kind, + MonoExprKind::Conversion { + kind: ConversionKind::Identity, + .. + } + )); +} + +#[test] +fn specialization_preserves_value_type_conversion_kinds() { + let (_db, output) = specialize_src( + r#" +type Wad is word; + +function main(x: word) returns (word) { + return (x as Wad) as word; +} +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains("main") => Some(function), + _ => None, + }) + .expect("specialized main function"); + let MonoStmtKind::Return(Some(ret)) = &main.body[0].kind else { + panic!("expected value-type return: {:#?}", main.body); + }; + let MonoExprKind::Conversion { + expr: inner, + kind: ConversionKind::ValueTypeUnwrap, + .. + } = &ret.kind + else { + panic!("expected outer value-type unwrap: {ret:#?}"); + }; + assert!(matches!( + inner.kind, + MonoExprKind::Conversion { + kind: ConversionKind::ValueTypeWrap, + .. + } + )); +} + +#[test] +fn specialization_uses_canonical_alias_targets_for_value_type_unwraps() { + let (db, output) = specialize_src( + r#" +alias W = word; +alias B = bool; + +type Wad is word; +type Flag is bool; + +function unwrap_word(x: Wad) returns (W) { + return x as W; +} + +function unwrap_bool(x: Flag) returns (B) { + return x as B; +} + +function main(x: word, b: bool) returns (word) { + let flag: Flag = b as Flag; + if (unwrap_bool(flag)) { + return unwrap_word(x as Wad); + } else { + return 0; + } +} +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + for (name, expected) in [ + ("unwrap_word", BuiltinTyCtor::Word), + ("unwrap_bool", BuiltinTyCtor::Bool), + ] { + let function = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains(name) => Some(function), + _ => None, + }) + .unwrap_or_else(|| panic!("specialized {name} function")); + let MonoStmtKind::Return(Some(ret)) = &function.body[0].kind else { + panic!( + "expected value-type return for {name}: {:#?}", + function.body + ); + }; + let MonoExprKind::Conversion { + ty: conversion_ty, + kind: ConversionKind::ValueTypeUnwrap, + .. + } = &ret.kind + else { + panic!("expected value-type unwrap for {name}: {ret:#?}"); + }; + let expected = Ty::builtin(db, expected); + assert_eq!(function.ret.ty(), expected, "{name} return type"); + assert_eq!(ret.ty.ty(), expected, "{name} expression type"); + assert_eq!(conversion_ty.ty(), expected, "{name} conversion type"); + } +} + +#[test] +fn specialization_rejects_fixed_array_runtime_types() { + let (db, output) = specialize_src( + r#" +function main(values: word[3]) returns (word[3]) { + return values; +} +"#, + ); + + let diagnostic = output + .diagnostics + .iter() + .find(|diagnostic| matches!( + &diagnostic.kind, + SpecializeDiagnosticKind::UnsupportedRuntimeType { ty, .. } if ty.contains("word[3]") + )) + .expect("fixed-array runtime diagnostic"); + let lowered = diagnostic.lower(db); + assert_eq!(lowered.code.as_deref(), Some("SC0416")); + assert!(lowered.message.contains("cannot represent `word[3]`")); + assert!( + output.module.items.iter().all(|item| !matches!( + item, + MonoItem::Function(function) if function.name.contains("main") + )), + "{:#?}", + output.module.items + ); +} + +#[test] +fn specialization_rejects_fixed_arrays_hidden_in_nominal_adt_layouts() { + let (db, output) = specialize_src( + r#" +struct Box { + values: word[3]; +} + +function main(value: Box) returns (Box) { + return value; +} +"#, + ); + + let diagnostic = output + .diagnostics + .iter() + .find(|diagnostic| { + matches!( + &diagnostic.kind, + SpecializeDiagnosticKind::UnsupportedRuntimeType { ty, .. } if ty.contains("Box") + ) + }) + .expect("nominal fixed-array layout diagnostic"); + let lowered = diagnostic.lower(db); + assert_eq!(lowered.code.as_deref(), Some("SC0416")); + assert!(lowered.message.contains("cannot represent")); + assert!( + output.module.items.iter().all(|item| !matches!( + item, + MonoItem::Function(function) if function.name.contains("main") + )), + "{:#?}", + output.module.items + ); +} + +#[test] +fn specialization_erases_internal_type_ascription() { + let (_db, output) = specialize_src( + r#" +alias Pair = (word, bool); + +function main(value: Pair) returns (word) { + let (head, flag): Pair = value; + return head; +} +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains("main") => Some(function), + _ => None, + }) + .expect("specialized main function"); + let MonoStmtKind::Match { scrutinees, .. } = &main.body[0].kind else { + panic!("expected lowered tuple match: {:#?}", main.body); + }; + assert!( + scrutinees + .iter() + .all(|expr| !matches!(expr.kind, MonoExprKind::Conversion { .. })), + "{scrutinees:#?}" + ); +} + +#[test] +fn named_struct_field_specializes_to_its_source_index() { + let (_db, output) = specialize_src( + r#" +struct Pair { + first: word; + second: word; +} + +function main(p: Pair) returns (word) { + return p.second; +} +"#, + ); + + assert!(output.diagnostics.is_empty(), "{:?}", output.diagnostics); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains("main") => Some(function), + _ => None, + }) + .expect("specialized main function"); + assert!(main.body.iter().any(|stmt| { + matches!( + &stmt.kind, + MonoStmtKind::Return(Some(MonoExpr { + kind: MonoExprKind::Field { field, .. }, + .. + })) if field == "1" + ) + })); +} + +fn specialize_source_at_root(root: &Path, rel_path: &str, src: &str) -> SpecializeOutput<'static> { + let db = Box::leak(Box::new(TestDb::default())); + let std_root = PathBuf::from("/std"); + db.module_tree = Some(ModuleTree::new( + db, + root.to_path_buf(), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [root, std_root.as_path()])); + let path = root.join(rel_path); + let key = module_key_for_path(LibraryId::Main, root, &path).expect("file under main root"); + let file = source_file_at_path(db, &path, src); + db.insert_module_file(key, file); + let module = parse_file_to_hir(db, file).module(db); + specialize_module(db, module, SpecializeOptions::default()) +} + +fn function_summaries(db: &TestDb, output: &SpecializeOutput<'_>) -> Vec { + let mut summaries = output + .module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => { + let params = function + .params + .iter() + .map(|param| param.ty.ty().display(db)) + .collect::>() + .join(", "); + Some(format!( + "{}({}) -> {}", + function.name, + params, + function.ret.ty().display(db) + )) + } + _ => None, + }) + .collect::>(); + summaries.sort(); + summaries +} + +#[test] +fn naming_matches_reference_mangling() { + let db = TestDb::default(); + let word = Ty::builtin(&db, BuiltinTyCtor::Word); + let pair = Ty::named( + &db, + hir_ty::TyCtor::Builtin(BuiltinTyCtor::Pair), + vec![word, Ty::builtin(&db, BuiltinTyCtor::Bool)], + ); + + assert_eq!(specialize_name(&db, "map", &[word]), "map$word"); + assert_eq!( + specialize_name(&db, "std.map", &[pair]), + "std_map$pairLword_boolJ" + ); + + let word_to_word = Ty::function(&db, vec![word], word); + let bool_to_word = Ty::function(&db, vec![Ty::builtin(&db, BuiltinTyCtor::Bool)], word); + assert_ne!( + specialize_name(&db, "apply", &[word_to_word]), + specialize_name(&db, "apply", &[bool_to_word]) + ); + + let fixed_four = Ty::fixed_array(&db, word, 4); + let fixed_five = Ty::fixed_array(&db, word, 5); + let fixed_four_name = specialize_name(&db, "map", &[fixed_four]); + let fixed_five_name = specialize_name(&db, "map", &[fixed_five]); + assert_ne!(fixed_four_name, fixed_five_name); + assert!(fixed_four_name.contains('4'), "{fixed_four_name}"); + assert!(fixed_five_name.contains('5'), "{fixed_five_name}"); +} + +#[test] +fn specialized_name_hash_is_independent_of_absolute_module_root() { + let src = r#" +contract C { + function main() public returns (word) { return 42; } +} +"#; + let left = specialize_source_at_root(Path::new("/workspace-a/project"), "src/main.solc", src); + let right = specialize_source_at_root(Path::new("/workspace-b/project"), "src/main.solc", src); + + assert_eq!(left.diagnostics, Vec::new()); + assert_eq!(right.diagnostics, Vec::new()); + assert_eq!(function_names(&left), function_names(&right)); +} + +#[test] +fn deduplicates_identical_instantiations() { + let (_db, output) = specialize_src( + r#" +function id(x: a) returns (a) { return x; } + +contract C { + function main(x: word) public returns (word) { + let a = id(x); + let b = id(a); + return b; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert_eq!( + names + .iter() + .filter(|name| name.contains("_id_") && name.ends_with("$word")) + .count(), + 1 + ); +} + +#[test] +fn evidence_replay_resolves_instance_and_superclass_methods() { + let (_db, output) = specialize_src( + r#" +enum Bool { True, False } + +trait Eq { + function eq(x: a, y: a) returns (Bool); +} + +trait Ord where a: Eq { + function lt(x: a, y: a) returns (Bool); +} + +impl Eq { + function eq(x: word, y: word) returns (Bool) { return primEqWord(x, y); } +} + +impl Ord { + function lt(x: word, y: word) returns (Bool) { return Bool.False; } +} + +function same(x: a) returns (Bool) where a: Ord { + return Eq.eq(x, x); +} + +contract C { + function main(x: word) public returns (Bool) { + return same(x); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.contains("_same_") && name.ends_with("$word")), + "{names:?}" + ); + assert!( + names + .iter() + .any(|name| name.starts_with("Eq_eq_d") && name.ends_with("$word")), + "{names:?}" + ); +} + +#[test] +fn evidence_replay_preserves_class_method_local_forall_binders() { + let (_db, output) = specialize_src( + r#" +trait IsA { + function ais(x: a, witness: b) returns (a); +} + +impl IsA { + function ais(x: a, witness: word) returns (a) { + return x; + } +} + +contract C { + function main(x: word) public returns (word) { + return IsA.ais(x, 0); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.contains("ais") && name.contains("$word")), + "{names:?}" + ); +} + +#[test] +fn evidence_replay_resolves_imported_instance_methods() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + let lib_path = main_root.join("lib.solc"); + let main_path = main_root.join("main.solc"); + let lib_file = source_file_at_path( + db, + &lib_path, + r#" +export { Boxed }; + +trait Boxed { + function id(x: a) returns (a); +} + +impl Boxed { + function id(x: word) returns (word) { return x; } +} +"#, + ); + let main_file = source_file_at_path( + db, + &main_path, + r#" +import {Boxed} from lib; + +contract C { + function main(x: word) public returns (word) { + return Boxed.id(x); + } +} +"#, + ); + let lib_key = module_key_for_path(LibraryId::Main, &main_root, &lib_path).unwrap(); + let main_key = module_key_for_path(LibraryId::Main, &main_root, &main_path).unwrap(); + db.insert_module_file(lib_key, lib_file); + db.insert_module_file(main_key, main_file); + + let module = parse_file_to_hir(db, main_file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.starts_with("Boxed_id_d") && name.ends_with("$word")), + "{names:?}" + ); +} + +#[test] +fn same_named_classes_in_different_modules_get_distinct_method_symbols() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + + let modules = [ + ( + "left.solc", + r#" +export { left }; + +trait Pick { + function choose(x: a) returns (word); +} + +impl Pick { + function choose(x: word) returns (word) { + let y: word; + assembly { y := sload(x) } + return y; + } +} + +function left(x: word) returns (word) { return Pick.choose(x); } +"#, + ), + ( + "right.solc", + r#" +export { right }; + +trait Pick { + function choose(x: a) returns (word); +} + +impl Pick { + function choose(x: word) returns (word) { + let y: word; + assembly { y := sload(x) } + return x; + } +} + +function right(x: word) returns (word) { return Pick.choose(x); } +"#, + ), + ( + "main.solc", + r#" +import {left} from left; +import {right} from right; + +contract C { + function main(x: word) public returns (word) { + let unused = right(x); + return left(x); + } +} +"#, + ), + ]; + + let mut main_file = None; + for (name, src) in modules { + let path = main_root.join(name); + let file = source_file_at_path(db, &path, src); + let key = module_key_for_path(LibraryId::Main, &main_root, &path).unwrap(); + db.insert_module_file(key, file); + if name == "main.solc" { + main_file = Some(file); + } + } + + let module = parse_file_to_hir(db, main_file.expect("main module")).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert_eq!(output.diagnostics, Vec::new()); + let method_names = function_names(&output) + .into_iter() + .filter(|name| name.starts_with("Pick_choose_d") && name.ends_with("$word")) + .collect::>(); + assert_eq!(method_names.len(), 2, "{method_names:?}"); +} + +#[test] +fn same_named_adts_in_different_modules_get_distinct_generic_symbols() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + + let modules = [ + ( + "common.solc", + r#" +export { id }; +function id(x: a) returns (a) { return x; } +"#, + ), + ( + "left.solc", + r#" +import {id} from common; +export { left }; +enum Foo { Foo(word) } +function left(x: word) returns (word) { + let value: Foo = id(Foo.Foo(x)); + match (value) { case Foo.Foo(result) { return result; } } +} +"#, + ), + ( + "right.solc", + r#" +import {id} from common; +export { right }; +enum Foo { Foo(word) } +function right(x: word) returns (word) { + let value: Foo = id(Foo.Foo(x)); + match (value) { case Foo.Foo(result) { return result; } } +} +"#, + ), + ( + "main.solc", + r#" +import {left} from left; +import {right} from right; +contract C { + function main(x: word) public returns (word) { + let unused = right(x); + return left(x); + } +} +"#, + ), + ]; + + let mut main_file = None; + for (name, src) in modules { + let path = main_root.join(name); + let file = source_file_at_path(db, &path, src); + let key = module_key_for_path(LibraryId::Main, &main_root, &path).unwrap(); + db.insert_module_file(key, file); + if name == "main.solc" { + main_file = Some(file); + } + } + + let module = parse_file_to_hir(db, main_file.expect("main module")).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert_eq!(output.diagnostics, Vec::new()); + let generic_names = function_names(&output) + .into_iter() + .filter(|name| name.contains("common_id_") && name.contains("$Foo_")) + .collect::>(); + assert_eq!(generic_names.len(), 2, "{generic_names:?}"); + assert_ne!(generic_names[0], generic_names[1], "{generic_names:?}"); +} + +#[test] +fn derived_generic_specialization_uses_the_imported_adt_definition_module() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots(db, [main_root.as_path()])); + let lib_path = main_root.join("lib.solc"); + let main_path = main_root.join("main.solc"); + let lib_file = source_file_at_path( + db, + &lib_path, + r#" +pragma solcore noPattersonCondition; +pragma solcore noBoundVariableCondition; + +export { Box(*), exercise }; + +trait Generic { + function from(x: a) returns (rep); + function to(x: rep) returns (a); +} + +enum Box { Box(word, bool) } + +function exercise(x: Box) returns (Box) { + let rep: (word, bool) = Generic.from(x); + return Generic.to(rep); +} +"#, + ); + let main_file = source_file_at_path( + db, + &main_path, + r#" +import lib; + +contract C { + function main(x: Box) returns (Box) { return exercise(x); } +} +"#, + ); + let lib_key = module_key_for_path(LibraryId::Main, &main_root, &lib_path).unwrap(); + let main_key = module_key_for_path(LibraryId::Main, &main_root, &main_path).unwrap(); + db.insert_module_file(lib_key, lib_file); + db.insert_module_file(main_key, main_file); + + let module = parse_file_to_hir(db, main_file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.starts_with("Generic_from_d") && name.contains("$Box_")), + "{names:?}" + ); + assert!( + names + .iter() + .any(|name| name.starts_with("Generic_to_d") && name.contains("$Box_")), + "{names:?}" + ); +} + +#[test] +fn invokable_invoke_replays_call_site_evidence() { + let (_db, output) = specialize_src( + r#" +function app(f: c, x: a) returns (b) where c: invokable { + return invokable.invoke(f, x); +} + +enum t_id { t_id } + +function impure(x: word) returns (word) { + let y: word; + assembly { y := sload(x) } + return y; +} + +impl invokable { + function invoke(self: t_id, x: word) returns (word) { + return impure(x); + } +} + +contract C { + function main(x: word) public returns (word) { + return app(t_id.t_id, x); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names.iter().any(|name| { + name.starts_with("invokable_invoke_d") + && name.contains("$t_id_") + && name.ends_with("_word_word") + }), + "{names:?}" + ); + assert!( + !output.module.items.iter().any(|item| match item { + MonoItem::Function(function) => function.body.iter().any(stmt_has_closure_dispatch), + _ => false, + }), + "{:?}", + output.module + ); +} + +#[test] +fn mptc_phantom_extras_recovered_before_naming_and_body_lowering() { + let (_db, output) = specialize_src( + r#" +enum Foo { Foo(word) } + +trait Encoder { + function encode(x: self, hint: word) returns (rep); +} + +trait Sink { + function sink(x: rep) returns (r); +} + +impl Encoder { + function encode(x: Foo, hint: word) returns (word) { + let y: word; + assembly { y := sload(hint) } + match (x) { case Foo.Foo(v) { return v; } } + } +} + +impl Sink { + function sink(x: word) returns (word) { + let y: word; + assembly { y := sload(x) } + return x; + } +} + +function f(x: a) returns (word) where a: Encoder, rep: Sink { + let r: rep = Encoder.encode(x, 0); + return Sink.sink(r); +} + +contract C { + function main(x: word) public returns (word) { + return f(Foo.Foo(x)); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names.iter().any(|name| { + name.starts_with("Encoder_encode_d") + && name.contains("$Foo_") + && name.ends_with("_word") + }), + "{names:?}" + ); + assert!( + names + .iter() + .any(|name| name.starts_with("Sink_sink_d") && name.ends_with("$word_word")), + "{names:?}" + ); + assert!( + !names.iter().any(|name| name.contains("$t")), + "unrecovered type variable in {names:?}" + ); +} + +#[test] +fn instance_method_names_include_the_complete_class_head() { + let (_db, output) = specialize_src( + r#" +enum Box { Box(word) } + +trait Convert { + function toRep(x: self) returns (rep); + function fromRep(x: rep) returns (self); +} + +impl Convert { + function toRep(x: Box) returns (word) { + match (x) { case Box.Box(w) { return w; } } + } + function fromRep(x: word) returns (Box) { + return Box.Box(x); + } +} + +function roundtrip(x: a) returns (a) where a: Convert { + let r: rep = Convert.toRep(x); + return Convert.fromRep(r); +} + +contract C { + function main(x: word) public returns (word) { + let b: Box = roundtrip(Box.Box(x)); + match (b) { case Box.Box(w) { return w; } } + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names.iter().any(|name| { + name.starts_with("Convert_toRep_d") && name.contains("$Box_") && name.ends_with("_word") + }), + "{names:?}" + ); + assert!( + names.iter().any(|name| { + name.starts_with("Convert_fromRep_d") + && name.contains("$Box_") + && name.ends_with("_word") + }), + "{names:?}" + ); +} + +#[test] +fn ensure_closed_failure_aborts_that_specialization() { + let (_db, output) = specialize_src( + r#" +function leak() returns (a) { + let y: a; + return y; +} + +contract C { + function main() public returns () { + let x = leak(); + return (); + } +} +"#, + ); + + assert!( + output.diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + SpecializeDiagnosticKind::FreeTypeVariable { .. } + )), + "{:?}", + output.diagnostics + ); + assert!( + !function_names(&output) + .iter() + .any(|name| name.contains("_leak_")), + "{:?}", + function_names(&output) + ); +} + +#[test] +fn generated_contract_dispatch_uses_explicit_std_dispatch_import() { + let source = r#" +import std; +import std.dispatch; + +contract C { + function answer() public returns (uint256) { return uint256.uint256(1); } +} +"#; + let output = specialize_src_with_std(source); + assert_eq!(output.diagnostics, Vec::new(), "{source}"); + let generated_contract = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("generated contract metadata"); + assert!(generated_contract.entries.iter().any(|entry| matches!( + entry, + MonoEntry::RuntimeMain { + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } + ))); + assert!( + generated_contract + .entries + .iter() + .all(|entry| !matches!(entry, MonoEntry::SelectorMethod { .. })) + ); +} + +#[test] +fn generated_contract_dispatch_rejects_abi_visible_comptime_params_before_runtime_rooting() { + let output = specialize_src_with_std( + r#" +import std; +import std.dispatch; + +contract C { + function answer(comptime x: word) public returns (word) { + return x; + } + + function other(comptime x: word) external view returns (word) { + return x; + } +} +"#, + ); + + assert_eq!( + output + .diagnostics + .iter() + .filter(|diagnostic| matches!( + diagnostic.kind, + SpecializeDiagnosticKind::PublicComptimeParam { .. } + )) + .count(), + 2, + "{:?}", + output.diagnostics + ); + let contract = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("contract metadata"); + assert!( + contract + .entries + .iter() + .all(|entry| !matches!(entry, MonoEntry::RuntimeMain { .. })), + "{:?}", + contract.entries + ); +} + +#[test] +fn generated_contract_dispatch_keeps_the_original_source_file() { + let src = r#" +import std; +import std.dispatch; + +contract C { + function answer() external view returns (uint256) { + return uint256.uint256(1); + } +} +"#; + let (db, file, output) = specialize_src_with_std_and_db(src); + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(file.content(db).as_deref(), Some(src)); + + let (source, specialized) = output + .module + .items + .iter() + .find_map(|item| { + let MonoItem::Contract(contract) = item else { + return None; + }; + contract.entries.iter().find_map(|entry| match entry { + MonoEntry::RuntimeMain { + source, + specialized, + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } => Some((*source, specialized.clone())), + _ => None, + }) + }) + .expect("compiler-owned dispatch main"); + assert_eq!(source.file(db), file); + assert_eq!( + source.fingerprint(db).as_deref(), + Some("solcore.generated.std_dispatch.main") + ); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name == specialized => Some(function), + _ => None, + }) + .expect("specialized compiler-owned dispatch main"); + assert_eq!(main.source, Some(source)); + assert_eq!(main.span.source_file(db), file); + + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.starts_with("dispatch_selector_matches")), + "{names:?}" + ); + assert!( + !output.module.items.iter().any(|item| match item { + MonoItem::Function(function) => function.body.iter().any(stmt_has_closure_dispatch), + _ => false, + }), + "{:?}", + output.module + ); +} + +#[test] +fn already_prepared_input_keeps_std_dispatch_origin() { + let src = r#" +import std; +import std.dispatch; + +contract C { + constructor(seed: uint256) payable { let saved = seed; } + function answer() public returns (uint256) { return uint256.uint256(1); } +} +"#; + let (db, file, _) = specialize_src_with_std_and_db(src); + let source = parse_file_to_hir(db, file).module(db); + let prepared = prepare_module(db, source); + let output = specialize_prepared_module(db, prepared, SpecializeOptions::default()); + assert_eq!(output.diagnostics, Vec::new()); + assert!(output.module.items.iter().any(|item| { + let MonoItem::Contract(contract) = item else { + return false; + }; + assert!(contract.constructor.explicit); + assert!(contract.constructor.payable); + assert_eq!(contract.constructor.inputs.len(), 1); + contract.entries.iter().any(|entry| { + matches!( + entry, + MonoEntry::RuntimeMain { + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } + ) + }) && contract + .entries + .iter() + .any(|entry| matches!(entry, MonoEntry::DeploymentMain { .. })) + })); +} + +#[test] +fn source_names_are_qualified_across_contracts() { + let (_db, output) = specialize_src( + r#" +contract A { function main() public returns (word) { return 1; } } +contract B { function main() public returns (word) { return 2; } } +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let entries = output + .module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Contract(contract) => Some(contract.entries.clone()), + _ => None, + }) + .flatten() + .collect::>(); + assert_eq!(entries.len(), 4, "{entries:?}"); + assert_eq!( + entries + .iter() + .filter(|entry| matches!(entry, MonoEntry::DeploymentMain { .. })) + .count(), + 2, + "{entries:?}" + ); + let specialized = entries + .iter() + .filter_map(|entry| match entry { + MonoEntry::RuntimeMain { + specialized, + origin: MonoRuntimeMainOrigin::User, + .. + } => Some(specialized.as_str()), + _ => None, + }) + .collect::>(); + assert_eq!(specialized.len(), 2, "{entries:?}"); + assert_ne!(specialized[0], specialized[1]); +} + +#[test] +fn dispatch_abi_shape_is_preserved_in_std_dispatch_mono_ir() { + let output = specialize_src_with_std( + r#" +import std; +import std.dispatch; + +contract PayableTest { + constructor() {} + function deposit() public payable returns (uint256) { return uint256.uint256(1); } + fallback() external payable {} +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let contract = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("contract"); + assert!(contract.entries.iter().any(|entry| matches!( + entry, + MonoEntry::RuntimeMain { + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } + ))); + assert!( + contract + .entries + .iter() + .any(|entry| matches!(entry, MonoEntry::DeploymentMain { .. })) + ); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.starts_with("dispatch_selector_matches")), + "{names:?}" + ); + assert!( + output.module.items.iter().any(|item| match item { + MonoItem::Function(function) => { + stmts_have_number_literal(&function.body, "3504541104") + } + _ => false, + }), + "deposit selector was not preserved in generated Mono IR" + ); + assert!(contract.constructor.explicit); + assert!(!contract.constructor.payable); + assert!(contract.fallback.explicit); + assert!(contract.fallback.payable); + assert!( + contract + .fallback + .specialized + .as_deref() + .is_some_and(|name| name.contains("_fallback_")) + ); +} + +#[test] +fn tuple_dispatch_uses_the_canonical_abi_selector() { + let output = specialize_src_with_std( + r#" +import std; +import std.dispatch; + +contract TupleSelector { + function pack(point: (uint256, uint256), tag: uint256) public returns (uint256) { + return tag; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let selector = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) + if function.name.starts_with("dispatch_selector_matches") + && function.name.contains("TupleSelector_pack") => + { + Some(function) + } + _ => None, + }) + .expect("tuple selector helper"); + assert!( + stmts_have_number_literal(&selector.body, "2780501819"), + "{selector:?}" + ); + assert!( + !stmts_have_number_literal(&selector.body, "2335799844"), + "{selector:?}" + ); +} + +#[test] +fn constructor_overlay_roots_three_argument_deployment_main() { + let output = specialize_src_with_std( + r#" +import std; +import std.dispatch; + +contract C { + constructor(x: uint256, y: uint256, z: uint256) { let saved = x; } + function main() returns () { return (); } +} +"#, + ); + assert_eq!(output.diagnostics, Vec::new()); + let contract = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("contract"); + assert!(contract.constructor.explicit); + assert_eq!(contract.constructor.inputs.len(), 3); + assert!( + contract + .entries + .iter() + .any(|entry| matches!(entry, MonoEntry::DeploymentMain { .. })) + ); + let names = function_names(&output); + assert!( + names.iter().any(|name| name.contains("_start")), + "{names:?}" + ); + assert!( + names + .iter() + .any(|name| name.contains("copy_arguments_for_constructor")), + "{names:?}" + ); +} + +#[test] +fn specializes_reference_constructor_and_dispatch_collision_regressions() { + let repo = repo_root(); + let corpus = repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch"); + for fixture in ["miniERC20.solc", "weth9.solc"] { + let output = specialize_fixture(&corpus.join(fixture)); + assert_eq!(output.diagnostics, Vec::new(), "{fixture}"); + } +} + +#[test] +fn mono_ir_carries_frontend_desugar_hook_plan() { + let repo = repo_root(); + let storage = specialize_fixture( + &repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/dispatch/storage.solc"), + ); + let lambda = specialize_fixture( + &repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/cases/SimpleLambda.solc"), + ); + let (_if_db, if_output) = specialize_src( + r#" +contract C { + function main() public returns (word) { + if (true) { return 1; } else { return 0; } + } +} +"#, + ); + + assert!(storage.diagnostics.is_empty(), "{:?}", storage.diagnostics); + assert!(lambda.diagnostics.is_empty(), "{:?}", lambda.diagnostics); + assert!( + if_output.diagnostics.is_empty(), + "{:?}", + if_output.diagnostics + ); + assert!(storage.module.frontend_desugar.bodies.iter().any(|body| { + body.transforms.iter().any(|transform| { + matches!( + transform, + hir_ty::FrontendTransform::FieldRead { hook, .. } if hook.contains("RVA.acc") + ) + }) + })); + assert!(storage.module.frontend_desugar.bodies.iter().any(|body| { + body.transforms.iter().any(|transform| { + matches!( + transform, + hir_ty::FrontendTransform::FieldWrite { hook, .. } if hook.contains("LVA.acc") + ) + }) + })); + assert!(lambda.module.frontend_desugar.bodies.iter().any(|body| { + body.transforms.iter().any(|transform| { + matches!( + transform, + hir_ty::FrontendTransform::IndirectCall { + evidence: Some(_), + .. + } + ) + }) + })); + assert!(if_output.module.frontend_desugar.bodies.iter().any(|body| { + body.transforms + .iter() + .any(|transform| matches!(transform, hir_ty::FrontendTransform::IfStmtToMatch { .. })) + })); +} + +#[test] +fn tuple_syntax_specializes_through_product_constructors() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(x: word, y: word, z: word) public returns (pair>) { + let t = (x, y, z); + match (t) { case (a, b, c) { return (a, b, c); } } + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let main = output + .module + .items + .iter() + .find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + function.name.contains("main").then_some(function) + }) + .expect("specialized main"); + let MonoStmtKind::Let { + init: Some(init), .. + } = &main.body[0].kind + else { + panic!("expected tuple let init: {:#?}", main.body); + }; + assert!(matches!(&init.kind, MonoExprKind::Con { ctor, .. } if ctor.name == "pair")); + + let MonoStmtKind::Match { arms, .. } = &main.body[1].kind else { + panic!("expected match over tuple binding: {:#?}", main.body); + }; + assert!(matches!(&arms[0].pats[0].kind, MonoPatKind::Con { ctor, .. } if ctor.name == "pair")); + + let MonoStmtKind::Return(Some(ret)) = &arms[0].body[0].kind else { + panic!("expected tuple return: {:#?}", arms[0].body); + }; + assert!(matches!(&ret.kind, MonoExprKind::Con { ctor, .. } if ctor.name == "pair")); +} + +#[test] +fn specializes_p7_cited_regression_corpus() { + let repo = repo_root(); + let corpus = repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples"); + for fixture in [ + "cases/app.solc", + "cases/mptc-chain-phantom.solc", + "cases/mptc-both-templates.solc", + "dispatch/nonpayable_ctor.solc", + "dispatch/storage.solc", + "cases/SimpleLambda.solc", + "dispatch/specialise_sum_of_product.solc", + ] { + let output = specialize_fixture(&corpus.join(fixture)); + assert_eq!(output.diagnostics, Vec::new(), "{fixture}"); + } + let basic = specialize_fixture(&corpus.join("dispatch/basic.solc")); + assert_eq!(basic.diagnostics, Vec::new(), "dispatch/basic.solc"); + assert!( + !basic.module.items.iter().any(|item| match item { + MonoItem::Function(function) => function.body.iter().any(stmt_has_closure_dispatch), + _ => false, + }), + "dispatch/basic.solc retained closure dispatch" + ); + let basic_contract = basic + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("basic contract metadata"); + assert!( + basic_contract.entries.iter().any(|entry| { + matches!( + entry, + MonoEntry::RuntimeMain { + specialized, + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } if specialized.contains("_C_main_") + ) + }), + "{:?}", + basic_contract.entries + ); + let payable = specialize_fixture(&corpus.join("dispatch/payable.solc")); + let payable_contract = payable + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Contract(contract) => Some(contract), + _ => None, + }) + .expect("payable contract metadata"); + assert!( + payable_contract.entries.iter().any(|entry| { + matches!( + entry, + MonoEntry::RuntimeMain { + specialized, + origin: MonoRuntimeMainOrigin::StdDispatch, + .. + } if specialized.contains("_main_") + ) + }), + "{:?}", + payable_contract.entries + ); + assert!(payable_contract.fallback.explicit); + assert!(payable_contract.fallback.payable); +} + +#[test] +fn folds_direct_function_compose_closure_fixture() { + let repo = repo_root(); + let output = specialize_fixture( + &repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/spec/06comp.solc"), + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), Some("42".to_owned())); +} + +const OPERATOR_CUSTOM_UINT_ADD: &str = r#" +import std; + +enum uint { u(word) } + +impl Add { + function add(x: uint, y: uint) returns (uint) { + return uint.u(42); + } +} + +function unwrap(x: uint) returns (word) { + match (x) { case uint.u(w) { return w; } } +} + +contract C { + function main() public returns (word) { + let a: uint = uint.u(1); + let b: uint = uint.u(2); + let c: uint = a + b; + return unwrap(c); + } +} +"#; + +const OPERATOR_METERS_ADD: &str = r#" +import std; + +enum meters { meters(word) } + +impl Add { + function add(x: meters, y: meters) returns (meters) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return meters.meters(addWord(xw, yw)); } } + } +} + +function unwrap(x: meters) returns (word) { + match (x) { case meters.meters(w) { return w; } } +} + +contract C { + function main() public returns (word) { + let a: meters = meters.meters(1); + let b: meters = meters.meters(2); + let c: meters = a + b; + return unwrap(c); + } +} +"#; + +const OPERATOR_METERS_ORD: &str = r#" +import std; + +enum meters { meters(word) } + +impl Eq { + function eq(x: meters, y: meters) returns (bool) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return eqWord(xw, yw); } } + } +} + +impl Ord { + function gt(x: meters, y: meters) returns (bool) { + match (x, y) { case (meters.meters(xw), meters.meters(yw)) { return gtWord(xw, yw); } } + } +} + +contract C { + function main() public returns (word) { + let a: meters = meters.meters(1); + let b: meters = meters.meters(2); + if (a < b) { + return 42; + } else { + return 0; + } + } +} +"#; + +const OPERATOR_CUSTOM_MUL: &str = r#" +import std; + +enum Weird { Weird(word) } + +impl Mul { + function mul(x: Weird, y: Weird) returns (Weird) { + return Weird.Weird(99); + } +} + +contract C { + function main() public returns (word) { + let result: Weird = Weird.Weird(2) * Weird.Weird(3); + match (result) { case Weird.Weird(value) { return value; } } + } +} +"#; + +const OPERATOR_CUSTOM_EQ: &str = r#" +import std; + +enum Weird { Weird(word) } + +impl Eq { + function eq(x: Weird, y: Weird) returns (bool) { + return false; + } +} + +contract C { + function main() public returns (word) { + if (Weird.Weird(1) == Weird.Weird(1)) { return 0; } else { return 99; } + } +} +"#; + +const OPERATOR_VISIBLE_BOOL_FUNCTIONS: &str = r#" +function and(x: bool, y: bool) returns (bool) { return false; } +function or(x: bool, y: bool) returns (bool) { return false; } +function not(x: bool) returns (bool) { return true; } + +contract C { + function main() public returns (word) { + if ((true && true) || !true) { return 0; } else { return 99; } + } +} +"#; + +const OPERATOR_WORD_ADD: &str = r#" +import std; + +contract C { + function main() public returns (word) { + return 1 + 2; + } +} +"#; + +#[test] +fn overloaded_binary_operators_specialize_through_instances() { + for (label, src, expected) in [ + ("custom uint Add", OPERATOR_CUSTOM_UINT_ADD, "42"), + ("meters Add", OPERATOR_METERS_ADD, "3"), + ("meters Ord", OPERATOR_METERS_ORD, "42"), + ("custom Mul", OPERATOR_CUSTOM_MUL, "99"), + ("custom Eq", OPERATOR_CUSTOM_EQ, "99"), + ("word Add", OPERATOR_WORD_ADD, "3"), + ] { + let output = specialize_src_with_std(src); + assert_eq!(output.diagnostics, Vec::new(), "{label}"); + assert_eq!( + main_return_number(&output), + Some(expected.to_owned()), + "{label}" + ); + } + + let (_db, output) = specialize_src(OPERATOR_VISIBLE_BOOL_FUNCTIONS); + assert_eq!(output.diagnostics, Vec::new(), "visible boolean functions"); + assert_eq!( + main_return_number(&output), + Some("0".to_owned()), + "logical operators use short-circuit semantics instead of visible functions" + ); +} + +#[test] +fn every_audited_operator_uses_its_selected_semantics() { + for (label, class, method, operator, expected) in [ + ("Div", "Div", "div", "/", "91"), + ("Mod", "Mod", "mod", "%", "92"), + ("BitAnd", "BitAnd", "band", "&", "93"), + ("BitXor", "BitXor", "bxor", "^", "94"), + ("BitOr", "BitOr", "bor", "|", "95"), + ] { + let src = format!( + r#" +import std; +enum Weird {{ Weird(word) }} +impl {class} {{ + function {method}(x: Weird, y: Weird) returns (Weird) {{ return Weird.Weird({expected}); }} +}} +contract C {{ + function main() public returns (word) {{ + let result: Weird = Weird.Weird(8) {operator} Weird.Weird(3); + match (result) {{ case Weird.Weird(value) {{ return value; }} }} + }} +}} +"# + ); + let output = specialize_src_with_std(&src); + assert_eq!(output.diagnostics, Vec::new(), "{label}"); + assert_eq!( + main_return_number(&output), + Some(expected.to_owned()), + "{label}" + ); + } + + let not_eq = specialize_src_with_std( + r#" +import std; +enum Weird { Weird(word) } +impl Eq { + function eq(x: Weird, y: Weird) returns (bool) { return true; } +} +contract C { + function main() public returns (word) { + if (Weird.Weird(1) != Weird.Weird(2)) { return 0; } else { return 96; } + } +} +"#, + ); + assert_eq!(not_eq.diagnostics, Vec::new(), "NotEq"); + assert_eq!(main_return_number(¬_eq), Some("96".to_owned()), "NotEq"); + + for (label, definition, expression, expected) in [ + ( + "And", + "function and(x: bool, y: bool) returns (bool) { return false; }", + "true && true", + "97", + ), + ( + "Or", + "function or(x: bool, y: bool) returns (bool) { return false; }", + "false || true", + "97", + ), + ( + "Not", + "function not(x: bool) returns (bool) { return true; }", + "!true", + "97", + ), + ] { + let src = format!( + r#" +{definition} +contract C {{ + function main() public returns (word) {{ + if ({expression}) {{ return 97; }} else {{ return 0; }} + }} +}} +"# + ); + let (_db, output) = specialize_src(&src); + assert_eq!(output.diagnostics, Vec::new(), "{label}"); + assert_eq!( + main_return_number(&output), + Some(expected.to_owned()), + "{label}" + ); + } +} + +#[test] +fn comptime_obligations_are_carried_into_mono_side_table() { + let (_db, output) = specialize_src( + r#" +function need(comptime x: word) returns (word) { return x; } + +contract C { + function main(x: word) public returns (comptime word) { + return need(x); + } +} +"#, + ); + + let obligations = output + .module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Function(function) => Some(function.comptime_obligations.clone()), + _ => None, + }) + .flatten() + .collect::>(); + assert!( + obligations + .iter() + .any(|obligation| matches!(obligation.kind, MonoComptimeObligationKind::Return { .. })), + "{obligations:?}" + ); + assert!( + obligations.iter().any(|obligation| matches!( + obligation.kind, + MonoComptimeObligationKind::CallParam { .. } + )), + "{obligations:?}" + ); +} + +#[test] +fn derived_generic_evidence_generates_from_body() { + let (_db, output) = specialize_src( + r#" +enum Pair { Pair(word, word) } + +trait Generic { + function from(x: a) returns (rep); + function to(x: rep) returns (a); +} + +contract C { + function main(x: Pair) public returns (pair) { + return Generic.from(x); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names.iter().any(|name| name.starts_with("Generic_from_d")), + "{names:?}" + ); +} + +#[test] +fn generic_abi_decoder_evidence_specializes_for_internal_sum_adt() { + let output = specialize_src_with_std( + r#" +import std; +import std.Generic; +import std.ABIGeneric; + +enum Choice { Left(uint256), Right(address) } + +contract C { + function main() returns (word) { + let buf = allocate_zeroed_memory(64); + let rdr: MemoryWordReader = MemoryWordReader.MemoryWordReader(buf); + let dec: ABIDecoder = ABIDecoder.ABIDecoder(rdr); + let value: Choice = decode(dec, 0); + match (value) { case Choice.Left(x) { return Typedef.rep(x); } case Choice.Right(_) { return 0; } } + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names.iter().any(|name| name.starts_with("Generic_to_d")), + "{names:?}" + ); + assert!( + names + .iter() + .any(|name| name.starts_with("ABIDecode_decode_d")), + "{names:?}" + ); +} + +#[test] +fn snapshot_small_specialized_module() { + let (db, output) = specialize_src( + r#" +function id(x: a) returns (a) { return x; } + +contract C { + function main(x: word) public returns (word) { + return id(x); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let summaries = function_summaries(db, &output); + assert_eq!(summaries.len(), 3, "{summaries:?}"); + assert!( + summaries + .iter() + .any(|summary| summary.contains("_id_") && summary.ends_with("(word) -> word")), + "{summaries:?}" + ); + assert!( + summaries + .iter() + .any(|summary| summary.contains("_main_") && summary.ends_with("(word) -> word")), + "{summaries:?}" + ); + assert!( + summaries + .iter() + .any(|summary| summary.contains("_start_") && summary.ends_with("() -> ()")), + "{summaries:?}" + ); +} + +#[test] +fn specializes_curated_typecheck_parity_corpus_files() { + let repo = repo_root(); + let corpus = repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples"); + for fixture in [ + "spec/00answer.solc", + "spec/06comp.solc", + "cases/super-class.solc", + ] { + let output = specialize_fixture(&corpus.join(fixture)); + assert_eq!(output.diagnostics, Vec::new(), "{fixture}"); + } +} + +#[test] +fn specializes_comptime_evaluation_corpus_verdicts() { + let repo = repo_root(); + let corpus = repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples"); + let passing = [ + "comptime/ct_asm_mem.solc", + "comptime/ct_chain_ok.solc", + "comptime/ct_let_ok.solc", + "comptime/ct_overloaded_ok.solc", + "comptime/ct_param_ok.solc", + "comptime/integer-basic.solc", + "comptime/integer-fib.solc", + "comptime/integer-lit-pat.solc", + "comptime/match_labels.solc", + "comptime/Plus.solc", + "comptime/string-lit-keccak.solc", + "comptime/string-lit-len.solc", + ]; + for fixture in passing { + let output = specialize_fixture(&corpus.join(fixture)); + assert_eq!(output.diagnostics, Vec::new(), "{fixture}"); + } +} + +#[test] +fn folds_recursive_comptime_integer_function() { + let (_db, output) = specialize_src( + r#" +function fib(comptime n: integer) returns (comptime integer) { + if (integerLt(n, 2)) { + return n; + } else { + return integerAdd(fib(integerSub(n, 1)), fib(integerSub(n, 2))); + } +} + +contract C { + function main() public returns (word) { + return wordFromInteger(fib(10)); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), Some("55".to_owned())); + let names = function_names(&output); + assert_eq!(names.len(), 2, "{names:?}"); + assert!( + names.iter().any(|name| name.contains("_main_")), + "{names:?}" + ); + assert!( + names.iter().any(|name| name.contains("_start_")), + "{names:?}" + ); +} + +#[test] +fn folds_comptime_yul_mstore_mload_subset() { + let (_db, output) = specialize_src( + r#" +function storeLoad(x: word) returns (word) { + let r: word; + assembly { + mstore(0, x) + r := mload(0) + } + return r; +} + +contract C { + function main() public returns (word) { + let comptime res: word = storeLoad(42); + return res; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), Some("42".to_owned())); +} + +#[test] +fn assembly_substitution_does_not_reuse_values_after_an_in_block_write() { + let (db, output) = specialize_src( + r#" +contract C { + function main(x: word) public returns (word) { + let a: word = 1; + assembly { + a := add(a, x) + a := add(a, a) + } + return a; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let function = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if function.name.contains("_main_") => Some(function), + _ => None, + }) + .expect("specialized main"); + let body = function + .body + .iter() + .find_map(|stmt| match &stmt.kind { + MonoStmtKind::Assembly(body) => Some(body), + _ => None, + }) + .expect("residual assembly"); + let YulStmtKind::Assign { value, .. } = &body[1].kind else { + panic!("expected second assignment, got {:?}", body[1].kind); + }; + let YulExprKind::Call { args, .. } = &value.kind else { + panic!("expected add call, got {:?}", value.kind); + }; + assert!(args.iter().all(|arg| { + matches!(&arg.kind, YulExprKind::Ident(name) if ident_text(db, name) == "a") + })); +} + +#[test] +fn does_not_fold_user_function_shadowing_std_literal_intrinsic() { + let (_db, output) = specialize_src( + r#" +function keccakLit(a: string) returns (word) { + return 0; +} + +contract C { + function main() public returns (word) { + return keccakLit("abc"); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), Some("0".to_owned())); +} + +#[test] +fn folds_resolved_std_string_keccak_literal_intrinsic() { + let repo = repo_root(); + let fixture = repo.join( + "crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/string-lit-keccak.solc", + ); + let output = specialize_fixture(&fixture); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + main_return_number(&output), + Some( + "35286403120855365962805127237049809881669876751651884979611909062921250761797" + .to_owned() + ) + ); +} + +#[test] +fn does_not_fold_user_addword_shadowing_builtin_wrapper_name() { + let (_db, output) = specialize_src( + r#" +function addWord(x: word, y: word) returns (word) { + let r: word; + assembly { r := sload(0) } + return r; +} + +contract C { + function main() public returns (word) { + return addWord(1, 2); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), None); +} + +#[test] +fn assignment_lhs_root_is_not_substituted() { + let repo = repo_root(); + let fixture = + repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/comptime/Plus.solc"); + let output = specialize_fixture(&fixture); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), Some("4".to_owned())); +} + +#[test] +fn compound_assignment_invalidates_lhs_root() { + let (_db, output) = specialize_src( + r#" +trait Add { + function add(l: t, r: t) returns (t); +} + +impl Add { + function add(l: word, r: word) returns (word) { + let result: word; + assembly { result := sload(0) } + return result; + } +} + +contract C { + function main() public returns (word) { + let x: word = 1; + x += 2; + return x; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), None); +} + +#[test] +fn unknown_if_invalidates_assignments_from_both_branches() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(c: bool) public returns (word) { + let x: word = 1; + if (c) { + } else { + x = 2; + } + return x; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!(main_return_number(&output), None); +} + +#[test] +fn if_statement_specializes_through_pre_typeck_match_view() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(c: bool) public returns (word) { + let x: word = 1; + if (c) { + x = 2; + } else { + x = 3; + } + return x; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let main = output + .module + .items + .iter() + .find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + function.name.contains("main").then_some(function) + }) + .expect("specialized main"); + let MonoStmtKind::Match { scrutinees, arms } = &main.body[1].kind else { + panic!( + "expected if statement to specialize as match: {:#?}", + main.body + ); + }; + assert_eq!(scrutinees.len(), 1); + assert_eq!(arms.len(), 2); + assert!(matches!(&arms[0].pats[0].kind, MonoPatKind::Con { ctor, .. } if ctor.name == "true")); + assert!(matches!(&arms[1].pats[0].kind, MonoPatKind::Con { ctor, .. } if ctor.name == "false")); + assert!(matches!(arms[0].body[0].kind, MonoStmtKind::Assign { .. })); + assert!(matches!(arms[1].body[0].kind, MonoStmtKind::Assign { .. })); +} + +#[test] +fn if_expression_specializes_through_pre_typeck_match_view() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(c: bool) public returns (word) { + let x: word = ((c) ? 2 : 3); + return x; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let main = output + .module + .items + .iter() + .find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + function.name.contains("main").then_some(function) + }) + .expect("specialized main"); + let MonoStmtKind::Let { + init: Some(init), .. + } = &main.body[0].kind + else { + panic!("expected if expression let init: {:#?}", main.body); + }; + let MonoExprKind::Match { scrutinee, arms } = &init.kind else { + panic!("expected if expression to specialize as match: {:#?}", init); + }; + assert!(matches!(&scrutinee.kind, MonoExprKind::Var(_))); + assert_eq!(arms.len(), 2); + assert!(matches!(&arms[0].pat.kind, MonoPatKind::Con { ctor, .. } if ctor.name == "true")); + assert!(matches!(&arms[1].pat.kind, MonoPatKind::Con { ctor, .. } if ctor.name == "false")); +} + +#[test] +fn bool_constructors_specialize_through_pre_typeck_unit_sum_view() { + let (_true_db, true_output) = specialize_src( + r#" +contract C { + function main() public returns (bool) { + return true; + } +} +"#, + ); + let (_false_db, false_output) = specialize_src( + r#" +contract C { + function main() public returns (bool) { + return false; + } +} +"#, + ); + + assert_eq!(true_output.diagnostics, Vec::new()); + assert_eq!(false_output.diagnostics, Vec::new()); + assert_eq!( + function_return_ctor(&true_output, "main"), + Some("true".to_owned()) + ); + assert_eq!( + function_return_ctor(&false_output, "main"), + Some("false".to_owned()) + ); +} + +#[test] +fn logical_binops_short_circuit_runtime_rhs_in_comptime_lets() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(flag: bool) public returns (bool) { + let comptime andResult: bool = false && flag; + let comptime orResult: bool = true || flag; + return andResult || orResult; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + function_return_ctor(&output, "main"), + Some("true".to_owned()) + ); +} + +#[test] +fn logical_binops_do_not_evaluate_unreachable_rhs() { + let db = Box::leak(Box::new(TestDb::default())); + let module = parse_module( + db, + r#" +function rhs() returns (bool) { return true; } + +function main() returns (bool) { + return (false && rhs()) || (true || rhs()); +} +"#, + ); + let output = specialize_module( + db, + module, + SpecializeOptions { + eval_fuel: 0, + ..SpecializeOptions::default() + }, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + function_return_ctor(&output, "main"), + Some("true".to_owned()) + ); +} + +#[test] +fn unknown_match_pattern_binders_shadow_outer_constants() { + let (_db, output) = specialize_src( + r#" +contract C { + function main(n: word) public returns (word) { + let x: word = 1; + match (n) { case x { return x; } } + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + function_return_numbers(&output, "main"), + Vec::::new() + ); +} + +#[test] +fn folds_qualified_constructor_matches_before_wildcard_defaults() { + let repo = repo_root(); + let corpus = repo.join("crates/parser/tests/fixtures/corpus/ok/test/examples/spec"); + for (fixture, expected) in [ + ("037dwarves.solc", "5"), + ("038food0.solc", "42"), + ("039food.solc", "42"), + ] { + let output = specialize_fixture(&corpus.join(fixture)); + assert_eq!(output.diagnostics, Vec::new(), "{fixture}"); + assert_eq!( + main_return_number(&output), + Some(expected.to_owned()), + "{fixture}" + ); + } +} + +fn main_return_number(output: &SpecializeOutput<'_>) -> Option { + let mut main_names = output + .module + .items + .iter() + .filter_map(|item| match item { + MonoItem::Contract(contract) => Some( + contract + .entries + .iter() + .filter_map(|entry| match entry { + MonoEntry::SelectorMethod { + name, specialized, .. + } if name == "main" => Some(specialized.clone()), + MonoEntry::RuntimeMain { specialized, .. } => Some(specialized.clone()), + _ => None, + }) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>(); + if main_names.is_empty() { + main_names = function_names(output) + .into_iter() + .filter(|name| name == "main" || name.contains("_main_")) + .collect(); + } + output.module.items.iter().find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + main_names.contains(&function.name).then(|| { + function.body.iter().find_map(|stmt| match &stmt.kind { + MonoStmtKind::Return(Some(expr)) => match &expr.kind { + MonoExprKind::Lit(hir::ast::function::LitKind::Number(value)) => { + Some(value.clone()) + } + _ => None, + }, + _ => None, + }) + })? + }) +} + +fn function_return_ctor(output: &SpecializeOutput<'_>, name: &str) -> Option { + output.module.items.iter().find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + function.name.contains(name).then(|| { + function.body.iter().find_map(|stmt| match &stmt.kind { + MonoStmtKind::Return(Some(expr)) => match &expr.kind { + MonoExprKind::Con { ctor, .. } => Some(ctor.name.clone()), + _ => None, + }, + _ => None, + }) + })? + }) +} + +fn stmts_have_number_literal(stmts: &[MonoStmt<'_>], expected: &str) -> bool { + stmts.iter().any(|stmt| match &stmt.kind { + MonoStmtKind::Let { init, .. } => init + .as_ref() + .is_some_and(|expr| expr_has_number_literal(expr, expected)), + MonoStmtKind::Return(expr) => expr + .as_ref() + .is_some_and(|expr| expr_has_number_literal(expr, expected)), + MonoStmtKind::Expr(expr) => expr_has_number_literal(expr, expected), + MonoStmtKind::Assign { lhs, rhs, .. } => { + expr_has_number_literal(lhs, expected) || expr_has_number_literal(rhs, expected) + } + MonoStmtKind::Match { scrutinees, arms } => { + scrutinees + .iter() + .any(|expr| expr_has_number_literal(expr, expected)) + || arms + .iter() + .any(|arm| stmts_have_number_literal(&arm.body, expected)) + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + stmts_have_number_literal(init, expected) + || expr_has_number_literal(cond, expected) + || stmts_have_number_literal(post, expected) + || stmts_have_number_literal(body, expected) + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + expr_has_number_literal(cond, expected) + || stmts_have_number_literal(then_body, expected) + || else_body + .as_ref() + .is_some_and(|body| stmts_have_number_literal(body, expected)) + } + MonoStmtKind::Block(body) => stmts_have_number_literal(body, expected), + MonoStmtKind::Assembly(_) + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Error => false, + }) +} + +fn expr_has_number_literal(expr: &MonoExpr<'_>, expected: &str) -> bool { + match &expr.kind { + MonoExprKind::Lit(hir::ast::function::LitKind::Number(value)) => value == expected, + MonoExprKind::Tuple(elems) => elems + .iter() + .any(|expr| expr_has_number_literal(expr, expected)), + MonoExprKind::Call { args, .. } | MonoExprKind::Con { args, .. } => args + .iter() + .any(|expr| expr_has_number_literal(expr, expected)), + MonoExprKind::ClosureDispatch { callee, args } => { + expr_has_number_literal(callee, expected) + || args + .iter() + .any(|expr| expr_has_number_literal(expr, expected)) + } + MonoExprKind::BinOp { lhs, rhs, .. } => { + expr_has_number_literal(lhs, expected) || expr_has_number_literal(rhs, expected) + } + MonoExprKind::UnaryOp { expr, .. } | MonoExprKind::Conversion { expr, .. } => { + expr_has_number_literal(expr, expected) + } + MonoExprKind::Index { base, index } | MonoExprKind::StorageIndex { base, index } => { + expr_has_number_literal(base, expected) || expr_has_number_literal(index, expected) + } + MonoExprKind::Field { base, .. } => expr_has_number_literal(base, expected), + MonoExprKind::Match { scrutinee, arms } => { + expr_has_number_literal(scrutinee, expected) + || arms + .iter() + .any(|arm| expr_has_number_literal(&arm.expr, expected)) + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => { + expr_has_number_literal(cond, expected) + || expr_has_number_literal(then_expr, expected) + || expr_has_number_literal(else_expr, expected) + } + MonoExprKind::Var(_) + | MonoExprKind::Lit(_) + | MonoExprKind::Proxy(_) + | MonoExprKind::Lambda { .. } + | MonoExprKind::Error => false, + } +} + +fn stmt_has_closure_dispatch(stmt: &MonoStmt<'_>) -> bool { + match &stmt.kind { + MonoStmtKind::Let { init, .. } => init.as_ref().is_some_and(expr_has_closure_dispatch), + MonoStmtKind::Return(expr) => expr.as_ref().is_some_and(expr_has_closure_dispatch), + MonoStmtKind::Expr(expr) => expr_has_closure_dispatch(expr), + MonoStmtKind::Assign { lhs, rhs, .. } => { + expr_has_closure_dispatch(lhs) || expr_has_closure_dispatch(rhs) + } + MonoStmtKind::Match { scrutinees, arms } => { + scrutinees.iter().any(expr_has_closure_dispatch) + || arms.iter().any(|arm| { + arm.pats.iter().any(pat_has_closure_dispatch) + || arm.body.iter().any(stmt_has_closure_dispatch) + }) + } + MonoStmtKind::For { + init, + cond, + post, + body, + } => { + init.iter().any(stmt_has_closure_dispatch) + || expr_has_closure_dispatch(cond) + || post.iter().any(stmt_has_closure_dispatch) + || body.iter().any(stmt_has_closure_dispatch) + } + MonoStmtKind::If { + cond, + then_body, + else_body, + } => { + expr_has_closure_dispatch(cond) + || then_body.iter().any(stmt_has_closure_dispatch) + || else_body + .as_ref() + .is_some_and(|body| body.iter().any(stmt_has_closure_dispatch)) + } + MonoStmtKind::Block(body) => body.iter().any(stmt_has_closure_dispatch), + MonoStmtKind::Assembly(_) + | MonoStmtKind::Break + | MonoStmtKind::Continue + | MonoStmtKind::Error => false, + } +} + +fn expr_has_closure_dispatch(expr: &MonoExpr<'_>) -> bool { + match &expr.kind { + MonoExprKind::ClosureDispatch { .. } => true, + MonoExprKind::Tuple(elems) => elems.iter().any(expr_has_closure_dispatch), + MonoExprKind::Call { args, .. } | MonoExprKind::Con { args, .. } => { + args.iter().any(expr_has_closure_dispatch) + } + MonoExprKind::BinOp { lhs, rhs, .. } => { + expr_has_closure_dispatch(lhs) || expr_has_closure_dispatch(rhs) + } + MonoExprKind::UnaryOp { expr, .. } | MonoExprKind::Conversion { expr, .. } => { + expr_has_closure_dispatch(expr) + } + MonoExprKind::Index { base, index } | MonoExprKind::StorageIndex { base, index } => { + expr_has_closure_dispatch(base) || expr_has_closure_dispatch(index) + } + MonoExprKind::Field { base, .. } => expr_has_closure_dispatch(base), + MonoExprKind::Match { scrutinee, arms } => { + expr_has_closure_dispatch(scrutinee) + || arms.iter().any(|arm| { + pat_has_closure_dispatch(&arm.pat) || expr_has_closure_dispatch(&arm.expr) + }) + } + MonoExprKind::If { + cond, + then_expr, + else_expr, + } => { + expr_has_closure_dispatch(cond) + || expr_has_closure_dispatch(then_expr) + || expr_has_closure_dispatch(else_expr) + } + MonoExprKind::Var(_) + | MonoExprKind::Lit(_) + | MonoExprKind::Proxy(_) + | MonoExprKind::Lambda { .. } + | MonoExprKind::Error => false, + } +} + +fn pat_has_closure_dispatch(pat: &solcore_specialize::MonoPat<'_>) -> bool { + match &pat.kind { + MonoPatKind::Con { args, .. } | MonoPatKind::Tuple(args) => { + args.iter().any(pat_has_closure_dispatch) + } + MonoPatKind::ComptimeLabel(expr) => expr_has_closure_dispatch(expr), + MonoPatKind::Wildcard | MonoPatKind::Var(_) | MonoPatKind::Lit(_) | MonoPatKind::Error => { + false + } + } +} + +fn function_return_numbers(output: &SpecializeOutput<'_>, name: &str) -> Vec { + output + .module + .items + .iter() + .find_map(|item| { + let MonoItem::Function(function) = item else { + return None; + }; + (function.name == name).then(|| return_numbers_in_stmts(&function.body)) + }) + .unwrap_or_default() +} + +fn return_numbers_in_stmts(stmts: &[solcore_specialize::MonoStmt<'_>]) -> Vec { + let mut out = Vec::new(); + for stmt in stmts { + match &stmt.kind { + MonoStmtKind::Return(Some(expr)) => { + if let MonoExprKind::Lit(hir::ast::function::LitKind::Number(value)) = &expr.kind { + out.push(value.clone()); + } + } + MonoStmtKind::Match { arms, .. } => { + for arm in arms { + out.extend(return_numbers_in_stmts(&arm.body)); + } + } + MonoStmtKind::If { + then_body, + else_body, + .. + } => { + out.extend(return_numbers_in_stmts(then_body)); + if let Some(else_body) = else_body { + out.extend(return_numbers_in_stmts(else_body)); + } + } + MonoStmtKind::For { + init, post, body, .. + } => { + out.extend(return_numbers_in_stmts(init)); + out.extend(return_numbers_in_stmts(post)); + out.extend(return_numbers_in_stmts(body)); + } + MonoStmtKind::Block(body) => out.extend(return_numbers_in_stmts(body)), + _ => {} + } + } + out +} + +fn specialize_fixture(path: &Path) -> SpecializeOutput<'static> { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = path.parent().expect("fixture parent").to_path_buf(); + let repo = repo_root(); + let std_root = repo.join("crates/parser/tests/fixtures/corpus/ok/std"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + db, + [main_root.as_path(), std_root.as_path()], + )); + let source = fs::read_to_string(path).expect("fixture source"); + let key = + module_key_for_path(LibraryId::Main, &main_root, path).expect("fixture under main root"); + let file = SourceFile::new( + db, + url::Url::from_file_path(path).expect("file URL"), + Some(source), + ); + db.insert_module_file(key.clone(), file); + let unresolved = load_reachable_modules(db, key.clone()); + assert!(unresolved.is_empty(), "{unresolved:?}"); + let module = parse_file_to_hir(db, file).module(db); + specialize_module(db, module, SpecializeOptions::default()) +} + +fn module_fs_snapshot_for_roots<'a>( + db: &TestDb, + roots: impl IntoIterator, +) -> ModuleFsSnapshot { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +fn load_reachable_modules(db: &mut TestDb, entry: ModuleKey) -> Vec { + let mut queue = VecDeque::from([entry]); + let mut visited = FxHashSet::default(); + let mut unresolved = Vec::new(); + + while let Some(key) = queue.pop_front() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = db.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map( + |path| match resolve_module_path_candidate(&*db, module, &path) { + Ok(resolved) => Some((resolved.module.key(&*db), resolved.file_path)), + Err(_) => { + unresolved.push(format!( + "{} imports `{}`", + module.display(&*db), + module_path_display(&*db, &path) + )); + None + } + }, + ) + .collect::>() + }; + for (target_key, file_path) in targets { + if !db.module_files.contains_key(&target_key) { + match fs::read_to_string(&file_path) { + Ok(source) => { + let file = SourceFile::new( + db, + url::Url::from_file_path(&file_path).expect("file URL"), + Some(source), + ); + db.insert_module_file(target_key.clone(), file); + } + Err(err) => unresolved.push(format!( + "failed to read {} for {}: {err}", + file_path.display(), + module_key_display(&target_key) + )), + } + } + if db.module_files.contains_key(&target_key) { + queue.push_back(target_key); + } + } + } + + unresolved.sort(); + unresolved.dedup(); + unresolved +} + +fn module_key_display(key: &ModuleKey) -> String { + let path = key.logical_path.join("."); + match &key.library { + LibraryId::Main => path, + LibraryId::Std if key.logical_path.as_slice() == ["std"] => "std".to_owned(), + LibraryId::Std => format!("std.{path}"), + LibraryId::External(name) => format!("@{name}.{path}"), + } +} + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("repo root") + .to_path_buf() +} + +#[test] +fn constructor_fold_is_not_confused_by_underscored_names() { + let (_db, output) = specialize_src( + r#" +enum D { Suf, Pre_Suf } + +function pick(d: D) returns (word) { + match (d) { case D.Suf { return 1; } case D.Pre_Suf { return 2; } } +} + +contract C { + function main() returns (word) { + return pick(D.Pre_Suf); + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + main_return_number(&output).as_deref(), + Some("2"), + "{:?}", + output.module + ); +} + +#[test] +fn for_loop_post_assignments_are_not_folded_to_preloop_constants() { + let (_db, output) = specialize_src( + r#" +enum Flag { On, Off } + +function isOn(f: Flag) returns (bool) { + match (f) { case Flag.On { return true; } case Flag.Off { return false; } } +} + +contract C { + function main() returns (word) { + let f: Flag = Flag.On; + for (; isOn(f); f = Flag.Off) { + } + return 1; + } +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let cond_is_residual = output.module.items.iter().any(|item| { + let MonoItem::Function(function) = item else { + return false; + }; + function.body.iter().any(|stmt| { + fn stmt_has_residual_for_cond(stmt: &MonoStmt<'_>) -> bool { + match &stmt.kind { + MonoStmtKind::For { cond, .. } => { + !matches!(cond.kind, MonoExprKind::Con { .. } | MonoExprKind::Lit(_)) + } + MonoStmtKind::Block(body) => body.iter().any(stmt_has_residual_for_cond), + _ => false, + } + } + stmt_has_residual_for_cond(stmt) + }) + }); + assert!(cond_is_residual, "{:?}", output.module); +} + +#[test] +fn non_contract_main_survives_dead_function_elimination_after_name_mangling() { + let (_db, output) = specialize_src( + r#" +function answer() returns (word) { return 42; } +function main() returns (word) { return answer(); } +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert_eq!( + main_return_number(&output).as_deref(), + Some("42"), + "{:?}", + output.module + ); + assert!( + function_names(&output) + .iter() + .any(|name| name.contains("_main_d")), + "{:?}", + output.module + ); +} + +#[test] +fn evaluator_fuel_bounds_total_inline_fanout_work() { + let db = Box::leak(Box::new(TestDb::default())); + let module = parse_module( + db, + r#" +function g2() returns (word) { return 1; } +function g1() returns (word) { return g2() + g2(); } +function g0() returns (word) { return g1() + g1(); } + +contract C { + function main() returns (word) { return g0(); } +} +"#, + ); + let output = specialize_module( + db, + module, + SpecializeOptions { + eval_fuel: 3, + ..SpecializeOptions::default() + }, + ); + + assert!( + output.diagnostics.iter().any(|diagnostic| matches!( + diagnostic.kind, + SpecializeDiagnosticKind::ReductionFuelExhausted { limit: 3, .. } + )), + "{:?}", + output.diagnostics + ); +} + +#[test] +fn dead_function_elimination_traces_calls_inside_residual_lambdas() { + let (_db, output) = specialize_src( + r#" +enum Box { Box(f) } + +function target(x: word) returns (word) { + let result: word; + assembly { result := add(x, 1) } + return result; +} + +function main() returns (Box) { + return Box.Box(lam (x: word) returns (word) { return target(x); }); +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert!( + function_names(&output) + .iter() + .any(|name| name.contains("_target_")), + "{:?}", + output.module + ); +} + +#[test] +fn dead_function_elimination_keeps_function_values_nested_in_constructors() { + let (_db, output) = specialize_src( + r#" +enum Box { Box(f) } + +function target(x: word) returns (word) { + let result: word; + assembly { result := add(x, 1) } + return result; +} + +function main() returns (Box) { + return Box.Box(target); +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert!( + function_names(&output) + .iter() + .any(|name| name.contains("_target_")), + "{:?}", + output.module + ); + assert!( + output.module.items.iter().any(|item| matches!( + item, + MonoItem::Function(function) + if function.body.iter().any(|stmt| matches!( + &stmt.kind, + MonoStmtKind::Return(Some(MonoExpr { + kind: MonoExprKind::Con { args, .. }, + .. + })) if args.iter().any(|arg| matches!( + &arg.kind, + MonoExprKind::Var(id) if id.name.contains("_target_") + )) + )) + )), + "expected the surviving reference to be a constructor-nested function value: {:?}", + output.module + ); +} + +#[test] +fn user_path_suffix_does_not_grant_std_dispatch_inlining() { + let output = specialize_source_at_root( + Path::new("/main"), + "mystd/dispatch.solc", + r#" +function clobber(value: word) returns () { + let observed: word; + assembly { observed := callvalue() } + return (); +} + +function main() returns (word) { + clobber(0); + return 7; +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + assert!( + function_names(&output) + .iter() + .any(|name| name.contains("_clobber_")), + "{:?}", + output.module + ); +} + +#[test] +fn std_dispatch_statement_inlining_preserves_lexical_scope() { + let db = Box::leak(Box::new(TestDb::default())); + let main_root = PathBuf::from("/main"); + let std_root = PathBuf::from("/std"); + db.module_tree = Some(ModuleTree::new( + db, + main_root.clone(), + std_root.clone(), + BTreeMap::new(), + )); + db.module_fs_snapshot = Some(module_fs_snapshot_for_roots( + db, + [main_root.as_path(), std_root.as_path()], + )); + let path = std_root.join("dispatch.solc"); + let key = module_key_for_path(LibraryId::Std, &std_root, &path).expect("std dispatch key"); + let file = source_file_at_path( + db, + &path, + r#" +function clobber() returns () { + let x: word = 1; + assembly { mstore(x, x) } + return (); +} + +function main(x: word) returns (word) { + clobber(); + return x; +} +"#, + ); + db.insert_module_file(key, file); + let module = parse_file_to_hir(db, file).module(db); + let output = specialize_module(db, module, SpecializeOptions::default()); + + assert_eq!(output.diagnostics, Vec::new()); + let entry = output + .module + .entry_points + .first() + .expect("main entry point"); + let main = output + .module + .items + .iter() + .find_map(|item| match item { + MonoItem::Function(function) if &function.name == entry => Some(function), + _ => None, + }) + .expect("specialized main"); + assert!( + main.body.iter().any(|stmt| matches!( + &stmt.kind, + MonoStmtKind::Block(body) + if body.iter().any(|stmt| matches!( + &stmt.kind, + MonoStmtKind::Let { id, .. } if id.name == "x" + )) + )), + "{:?}", + main.body + ); +} + +#[test] +fn class_method_values_resolve_to_the_specialized_instance_method() { + let (_db, output) = specialize_src( + r#" +trait Pick { + function pick(x: t) returns (t); +} + +impl Pick { + function pick(x: word) returns (word) { + let result: word; + assembly { result := add(x, 1) } + return result; + } +} + +function main(x: word) returns (word) { + let f: function(word) returns (word) = Pick.pick; + return f(x); +} +"#, + ); + + assert_eq!(output.diagnostics, Vec::new()); + let names = function_names(&output); + assert!( + names + .iter() + .any(|name| name.contains("Pick_pick_") && name.contains("$word")), + "{names:?}" + ); + assert!( + !output.module.items.iter().any(|item| matches!( + item, + MonoItem::Function(function) + if function.body.iter().any(stmt_has_closure_dispatch) + )), + "{:?}", + output.module + ); +} diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 00000000..87d6cfd6 --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "solcore-test-utils" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +annotate-snippets = { workspace = true } +hir = { workspace = true } +insta = "1.43.2" +nameres = { workspace = true } +parser = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +serde_json = "1" +url = { workspace = true } diff --git a/crates/test-utils/src/e2e.rs b/crates/test-utils/src/e2e.rs new file mode 100644 index 00000000..da4fef15 --- /dev/null +++ b/crates/test-utils/src/e2e.rs @@ -0,0 +1,1108 @@ +//! Shared support for backend-to-EVM end-to-end tests. +//! +//! The helpers in this module deliberately start at bytecode. Each backend owns +//! its source-to-bytecode pipeline, while process management, Anvil execution, +//! directive parsing, and static-ABI call execution live here. + +use std::{ + env, fmt, + io::{BufRead, BufReader, Read, Write}, + net::TcpStream, + path::{Path, PathBuf}, + process::{Child, Command, ExitStatus, Stdio}, + sync::{Arc, Mutex, OnceLock, mpsc}, + thread, + time::{Duration, Instant}, +}; + +mod directive; + +pub use directive::*; + +pub const COMMAND_TIMEOUT: Duration = Duration::from_secs(30); +const ANVIL_START_TIMEOUT: Duration = Duration::from_secs(15); +const ANVIL_READY_TIMEOUT: Duration = Duration::from_secs(10); +const ANVIL_PRIVATE_KEY: &str = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FailureKind { + Directive, + Pipeline, + Tooling, + Solc, + Codegen, + Deploy, + Transaction, + Call, + Decode, + Mismatch, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct E2eFailure { + pub kind: FailureKind, + pub message: String, +} + +impl E2eFailure { + pub fn new(kind: FailureKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + } + } +} + +impl fmt::Display for E2eFailure { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{:?}: {}", self.kind, self.message) + } +} + +impl std::error::Error for E2eFailure {} + +pub fn e2e_enabled() -> bool { + env_flag("E2E") || e2e_required() +} + +pub fn e2e_pipeline_only() -> bool { + // A required run must never become green without exercising the EVM just + // because a pipeline-only flag leaked into the environment. + env_flag("E2E_PIPELINE_ONLY") && !e2e_required() +} + +pub fn e2e_required() -> bool { + env_flag("E2E_REQUIRED") +} + +fn env_flag(name: &str) -> bool { + env::var_os(name).as_deref() == Some(std::ffi::OsStr::new("1")) +} + +pub fn encode_hex(bytes: &[u8]) -> String { + let mut encoded = String::with_capacity(bytes.len() * 2); + for byte in bytes { + use fmt::Write as _; + write!(encoded, "{byte:02x}").expect("writing to a String cannot fail"); + } + encoded +} + +/// Returns whether `value` is a non-empty, whole-byte hexadecimal string. +pub fn looks_like_hex(value: &str) -> bool { + !value.is_empty() + && value.len().is_multiple_of(2) + && value.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +pub struct EvmHarness { + cast: PathBuf, + anvil: Anvil, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum CallOutcome { + Return(String), + Revert(Option>), +} + +impl EvmHarness { + /// Starts the shared EVM runtime when execution E2E is enabled. + /// + /// Missing tools and startup failures retain the historical local skip + /// behavior, unless `E2E_REQUIRED=1` makes the execution environment a + /// required part of the test (as CI should do). + pub fn from_env() -> Result, E2eFailure> { + if !e2e_enabled() || e2e_pipeline_only() { + return Ok(None); + } + + let cast = foundry_tool_path("CAST", "cast"); + if !command_available(&cast) { + return unavailable(format!( + "cast not found at {}; set CAST=/path/to/cast", + cast.display() + )); + } + let anvil_path = foundry_tool_path("ANVIL", "anvil"); + if !command_available(&anvil_path) { + return unavailable(format!( + "anvil not found at {}; set ANVIL=/path/to/anvil", + anvil_path.display() + )); + } + + match Anvil::spawn(&anvil_path, &cast) { + Ok(anvil) => Ok(Some(Self { cast, anvil })), + Err(message) => unavailable(message), + } + } + + pub fn url(&self) -> &str { + self.anvil.url() + } + + pub fn logs(&self) -> String { + self.anvil.logs() + } + + pub fn deploy(&self, bytecode: &str) -> Result { + let create_arg = format!("0x{bytecode}"); + let output = run_command( + &self.cast, + &[ + "send", + "--rpc-url", + self.url(), + "--private-key", + ANVIL_PRIVATE_KEY, + "--create", + &create_arg, + "--json", + ], + &[], + COMMAND_TIMEOUT, + ) + .map_err(|message| E2eFailure::new(FailureKind::Deploy, message))?; + if !output.status.success() { + return Err(E2eFailure::new( + FailureKind::Deploy, + format!( + "cast send failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ), + )); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + extract_json_string(&stdout, "contractAddress").ok_or_else(|| { + E2eFailure::new( + FailureKind::Deploy, + format!("cast send output did not contain contractAddress:\n{stdout}"), + ) + }) + } + + pub fn call(&self, address: &str, calldata: &str) -> Result { + match self.call_outcome(address, calldata)? { + CallOutcome::Return(returndata) => Ok(returndata), + CallOutcome::Revert(payload) => Err(E2eFailure::new( + FailureKind::Call, + format!( + "eth_call reverted{}", + payload + .as_deref() + .map(|data| format!(" with 0x{}", encode_hex(data))) + .unwrap_or_default() + ), + )), + } + } + + fn call_outcome(&self, address: &str, calldata: &str) -> Result { + let request = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_call", + "params": [{ "to": address, "data": calldata }, "latest"], + }); + let response = post_json(self.url(), &request.to_string())?; + decode_eth_call_response(&response) + } + + /// Submits one state-changing call and waits for a successful receipt. + pub fn send(&self, address: &str, calldata: &str) -> Result<(), E2eFailure> { + let output = run_command( + &self.cast, + &[ + "send", + "--rpc-url", + self.url(), + "--private-key", + ANVIL_PRIVATE_KEY, + address, + calldata, + "--json", + ], + &[], + COMMAND_TIMEOUT, + ) + .map_err(|message| E2eFailure::new(FailureKind::Transaction, message))?; + if !output.status.success() { + return Err(E2eFailure::new( + FailureKind::Transaction, + format!( + "cast send failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ), + )); + } + require_successful_transaction_receipt(&output.stdout) + } + + /// Compares raw EVM returndata with a directive expectation without + /// narrowing 256-bit ABI words to a host integer. + pub fn assert_return_data( + &self, + label: &str, + expected: &[u8], + returndata: &str, + ) -> Result<(), E2eFailure> { + assert_return_data(label, expected, returndata) + } + + /// Deploys one contract and executes every resolved directive against it. + /// + /// Ordinary directives use `eth_call`; `#[send(...)]` directives submit a + /// transaction whose state is visible to subsequent directives. Raw + /// JSON-RPC responses are inspected for call/revert assertions, while send + /// directives require a successful mined receipt. + pub fn execute_deployed_calls( + &self, + bytecode: &str, + calls: &[ResolvedE2eCall], + ) -> Result<(), E2eFailure> { + if calls.is_empty() { + return Err(E2eFailure::new( + FailureKind::Directive, + "fixture contains no E2E directives", + )); + } + let address = self.deploy(bytecode)?; + for (index, call) in calls.iter().enumerate() { + let label = format!( + "{} directive #{} [{}]", + call.signature, + index + 1, + call.calldata + ); + match &call.action { + ResolvedE2eAction::Call(expected) => { + let outcome = self + .call_outcome(&address, &call.calldata) + .map_err(|error| { + E2eFailure::new(error.kind, format!("{label}: {}", error.message)) + })?; + assert_call_outcome(&label, expected, &outcome)?; + } + ResolvedE2eAction::Send => { + self.send(&address, &call.calldata).map_err(|error| { + E2eFailure::new(error.kind, format!("{label}: {}", error.message)) + })?; + } + } + } + Ok(()) + } +} + +fn require_successful_transaction_receipt(stdout: &[u8]) -> Result<(), E2eFailure> { + let receipt: serde_json::Value = serde_json::from_slice(stdout).map_err(|error| { + E2eFailure::new( + FailureKind::Transaction, + format!("invalid cast send JSON receipt: {error}"), + ) + })?; + let status = receipt.get("status").ok_or_else(|| { + E2eFailure::new( + FailureKind::Transaction, + format!("cast send receipt has no status: {receipt}"), + ) + })?; + let succeeded = match status { + serde_json::Value::Number(number) => number.as_u64() == Some(1), + serde_json::Value::String(quantity) => parse_rpc_quantity(quantity) == Some(1), + _ => false, + }; + if succeeded { + return Ok(()); + } + Err(E2eFailure::new( + FailureKind::Transaction, + format!("transaction receipt is not successful: {receipt}"), + )) +} + +fn parse_rpc_quantity(quantity: &str) -> Option { + let digits = quantity + .strip_prefix("0x") + .or_else(|| quantity.strip_prefix("0X"))?; + (!digits.is_empty()) + .then(|| u64::from_str_radix(digits, 16).ok()) + .flatten() +} + +fn post_json(url: &str, body: &str) -> Result, E2eFailure> { + let url = url::Url::parse(url).map_err(|error| { + E2eFailure::new(FailureKind::Tooling, format!("invalid Anvil URL: {error}")) + })?; + if url.scheme() != "http" { + return Err(E2eFailure::new( + FailureKind::Tooling, + format!("unsupported Anvil URL scheme `{}`", url.scheme()), + )); + } + let host = url + .host_str() + .ok_or_else(|| E2eFailure::new(FailureKind::Tooling, "Anvil URL has no host"))?; + let port = url + .port_or_known_default() + .ok_or_else(|| E2eFailure::new(FailureKind::Tooling, "Anvil URL has no port"))?; + let mut stream = TcpStream::connect((host, port)).map_err(|error| { + E2eFailure::new( + FailureKind::Call, + format!("failed to connect to Anvil JSON-RPC: {error}"), + ) + })?; + stream + .set_read_timeout(Some(COMMAND_TIMEOUT)) + .and_then(|()| stream.set_write_timeout(Some(COMMAND_TIMEOUT))) + .map_err(|error| { + E2eFailure::new( + FailureKind::Call, + format!("failed to configure Anvil JSON-RPC socket: {error}"), + ) + })?; + let mut request_target = if url.path().is_empty() { + "/".to_owned() + } else { + url.path().to_owned() + }; + if let Some(query) = url.query() { + request_target.push('?'); + request_target.push_str(query); + } + let host_header = match url.host() { + Some(url::Host::Ipv6(address)) => format!("[{address}]:{port}"), + _ => format!("{host}:{port}"), + }; + // Anvil is a loopback server spawned by this harness. HTTP/1.0 plus + // `Connection: close` deliberately constrains response framing to a + // fixed-length or close-delimited body; generic/chunked HTTP belongs in a + // real HTTP client, not in this test harness. + write!( + stream, + "POST {request_target} HTTP/1.0\r\nHost: {host_header}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .and_then(|()| stream.flush()) + .map_err(|error| { + E2eFailure::new( + FailureKind::Call, + format!("failed to send Anvil JSON-RPC request: {error}"), + ) + })?; + + let mut response = Vec::new(); + stream.read_to_end(&mut response).map_err(|error| { + E2eFailure::new( + FailureKind::Call, + format!("failed to read Anvil JSON-RPC response: {error}"), + ) + })?; + parse_http_response(response) +} + +fn parse_http_response(response: Vec) -> Result, E2eFailure> { + let header_end = response + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|position| position + 4) + .ok_or_else(|| E2eFailure::new(FailureKind::Call, "malformed HTTP response from Anvil"))?; + let headers = std::str::from_utf8(&response[..header_end - 4]) + .map_err(|_| E2eFailure::new(FailureKind::Call, "non-UTF-8 HTTP headers from Anvil"))?; + let mut lines = headers.split("\r\n"); + let status_line = lines + .next() + .ok_or_else(|| E2eFailure::new(FailureKind::Call, "missing HTTP status from Anvil"))?; + let mut status_fields = status_line.split_ascii_whitespace(); + let version = status_fields.next().unwrap_or_default(); + let status = status_fields + .next() + .and_then(|status| status.parse::().ok()); + if !version.starts_with("HTTP/1.") || status.is_none() { + return Err(E2eFailure::new( + FailureKind::Call, + format!("malformed HTTP status from Anvil: `{status_line}`"), + )); + } + if status != Some(200) { + return Err(E2eFailure::new( + FailureKind::Call, + format!("Anvil JSON-RPC returned `{status_line}`"), + )); + } + + let mut content_length = None; + for line in lines { + let (name, value) = line.split_once(':').ok_or_else(|| { + E2eFailure::new( + FailureKind::Call, + format!("malformed HTTP header from Anvil: `{line}`"), + ) + })?; + let value = value.trim(); + if name.eq_ignore_ascii_case("transfer-encoding") && !value.eq_ignore_ascii_case("identity") + { + return Err(E2eFailure::new( + FailureKind::Call, + format!("unsupported Anvil HTTP transfer encoding `{value}`"), + )); + } + if name.eq_ignore_ascii_case("content-length") { + let parsed = value.parse::().map_err(|_| { + E2eFailure::new( + FailureKind::Call, + format!("invalid Anvil HTTP Content-Length `{value}`"), + ) + })?; + if content_length + .replace(parsed) + .is_some_and(|prior| prior != parsed) + { + return Err(E2eFailure::new( + FailureKind::Call, + "conflicting Anvil HTTP Content-Length headers", + )); + } + } + } + + let body = &response[header_end..]; + if let Some(expected) = content_length + && body.len() != expected + { + return Err(E2eFailure::new( + FailureKind::Call, + format!( + "truncated Anvil HTTP body: Content-Length is {expected}, received {} bytes", + body.len() + ), + )); + } + Ok(body.to_vec()) +} + +fn decode_eth_call_response(response: &[u8]) -> Result { + let response: serde_json::Value = serde_json::from_slice(response).map_err(|error| { + E2eFailure::new( + FailureKind::Call, + format!("invalid Anvil JSON-RPC response: {error}"), + ) + })?; + let object = response.as_object().ok_or_else(|| { + E2eFailure::new( + FailureKind::Call, + "Anvil JSON-RPC response is not an object", + ) + })?; + if object.get("jsonrpc").and_then(serde_json::Value::as_str) != Some("2.0") + || object.get("id").and_then(serde_json::Value::as_u64) != Some(1) + { + return Err(E2eFailure::new( + FailureKind::Call, + format!("invalid Anvil JSON-RPC envelope: {response}"), + )); + } + + match (object.get("result"), object.get("error")) { + (Some(result), None) => { + let result = result.as_str().ok_or_else(|| { + E2eFailure::new(FailureKind::Decode, "eth_call result is not a hex string") + })?; + decode_rpc_hex(result).map_err(|message| { + E2eFailure::new( + FailureKind::Decode, + format!("invalid eth_call result: {message}"), + ) + })?; + return Ok(CallOutcome::Return(result.to_owned())); + } + (None, Some(_)) => {} + _ => { + return Err(E2eFailure::new( + FailureKind::Call, + format!( + "Anvil JSON-RPC response must contain exactly one of result or error: {response}" + ), + )); + } + } + + let error = object + .get("error") + .and_then(serde_json::Value::as_object) + .ok_or_else(|| { + E2eFailure::new( + FailureKind::Call, + format!("Anvil JSON-RPC error is not an object: {response}"), + ) + })?; + let code = error + .get("code") + .and_then(serde_json::Value::as_i64) + .ok_or_else(|| E2eFailure::new(FailureKind::Call, "JSON-RPC error has no integer code"))?; + let message = error + .get("message") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + E2eFailure::new(FailureKind::Call, "JSON-RPC error has no string message") + })?; + let is_revert = code == 3 + || message + .to_ascii_lowercase() + .starts_with("execution reverted"); + if !is_revert { + return Err(E2eFailure::new( + FailureKind::Call, + format!("Anvil JSON-RPC error {code}: {message}"), + )); + } + let payload = find_hex_data(error.get("data")) + .transpose() + .map_err(|message| { + E2eFailure::new( + FailureKind::Decode, + format!("invalid revert data: {message}"), + ) + })?; + Ok(CallOutcome::Revert(payload)) +} + +fn find_hex_data(value: Option<&serde_json::Value>) -> Option, String>> { + match value? { + serde_json::Value::String(data) => Some(decode_rpc_hex(data)), + serde_json::Value::Object(object) => find_hex_data(object.get("data")), + _ => None, + } +} + +fn decode_rpc_hex(data: &str) -> Result, String> { + if !data.starts_with("0x") { + return Err("JSON-RPC hex data must start with `0x`".to_owned()); + } + decode_hex_data(data) +} + +fn assert_call_outcome( + label: &str, + expected: &ResolvedExpectedOutcome, + outcome: &CallOutcome, +) -> Result<(), E2eFailure> { + match (expected, outcome) { + (ResolvedExpectedOutcome::Return(expected), CallOutcome::Return(returndata)) => { + assert_return_data(label, expected, returndata) + } + (ResolvedExpectedOutcome::Return(_), CallOutcome::Revert(payload)) => Err(E2eFailure::new( + FailureKind::Call, + format!( + "{label}: unexpected revert{}", + payload + .as_deref() + .map(|data| format!(" with 0x{}", encode_hex(data))) + .unwrap_or_default() + ), + )), + (ResolvedExpectedOutcome::Revert(None), CallOutcome::Revert(_)) => Ok(()), + (ResolvedExpectedOutcome::Revert(Some(expected)), CallOutcome::Revert(Some(actual))) + if expected == actual => + { + Ok(()) + } + (ResolvedExpectedOutcome::Revert(Some(expected)), CallOutcome::Revert(actual)) => { + Err(E2eFailure::new( + FailureKind::Mismatch, + format!( + "{label}: expected revert payload 0x{}, got {}", + encode_hex(expected), + actual + .as_deref() + .map(|data| format!("0x{}", encode_hex(data))) + .unwrap_or_else(|| "no payload".to_owned()) + ), + )) + } + (ResolvedExpectedOutcome::Revert(_), CallOutcome::Return(returndata)) => { + Err(E2eFailure::new( + FailureKind::Mismatch, + format!("{label}: expected revert, call returned `{returndata}`"), + )) + } + } +} + +static SHARED_EVM_HARNESS: OnceLock>, E2eFailure>> = + OnceLock::new(); + +/// Serializes access to one process-wide Anvil harness. +/// +/// `None` preserves the local optional-tool behavior of +/// [`EvmHarness::from_env`]. With `E2E_REQUIRED=1`, initialization failures are +/// returned and the closure is not called. Callers should finish compilation +/// before entering this closure so parallel `dir-test` cases only serialize +/// the deploy/call section. +pub fn with_shared_evm_harness( + run: impl FnOnce(Option<&EvmHarness>) -> Result, +) -> Result { + let harness = SHARED_EVM_HARNESS.get_or_init(|| EvmHarness::from_env().map(Mutex::new)); + let harness = match harness { + Ok(harness) => harness, + Err(error) => return Err(error.clone()), + }; + let guard = harness.lock().map_err(|_| { + E2eFailure::new( + FailureKind::Tooling, + "shared EVM harness lock was poisoned by an earlier E2E failure", + ) + })?; + run(guard.as_ref()) +} + +fn unavailable(message: String) -> Result, E2eFailure> { + if e2e_required() { + Err(E2eFailure::new(FailureKind::Tooling, message)) + } else { + eprintln!("skipping E2E: {message}"); + Ok(None) + } +} + +/// Compares exact ABI returndata bytes. +/// +/// This supports the full EVM word range and static tuple layouts. The +/// directive resolver produces the expected bytes through +/// [`resolve_e2e_directive`]. +pub fn assert_return_data( + label: &str, + expected: &[u8], + returndata: &str, +) -> Result<(), E2eFailure> { + let actual = decode_hex_data(returndata).map_err(|message| { + E2eFailure::new( + FailureKind::Decode, + format!("{label}: failed to decode `{returndata}`: {message}"), + ) + })?; + if actual == expected { + return Ok(()); + } + + Err(E2eFailure::new( + FailureKind::Mismatch, + format!( + "{label}: expected 0x{}, got 0x{}", + encode_hex(expected), + encode_hex(&actual) + ), + )) +} + +/// Decodes an optionally `0x`-prefixed, even-length hexadecimal byte string. +pub fn decode_hex_data(data: &str) -> Result, String> { + let data = data.trim(); + let hex = data.strip_prefix("0x").unwrap_or(data); + if !hex.len().is_multiple_of(2) { + return Err(format!( + "expected an even number of hex characters, got {}", + hex.len() + )); + } + if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err("data is not hexadecimal".to_owned()); + } + + hex.as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).expect("ASCII hex is UTF-8"); + u8::from_str_radix(pair, 16).map_err(|error| error.to_string()) + }) + .collect() +} + +fn extract_json_string(output: &str, key: &str) -> Option { + let key = format!("\"{key}\""); + let start = output.find(&key)?; + let after_key = output[start + key.len()..].find(':')? + start + key.len() + 1; + let after_quote = output[after_key..].find('"')? + after_key + 1; + let end = output[after_quote..].find('"')? + after_quote; + Some(output[after_quote..end].to_owned()) +} + +pub struct CommandOutput { + pub status: ExitStatus, + pub stdout: Vec, + pub stderr: Vec, +} + +pub fn run_command( + command: &Path, + args: &[&str], + path_args: &[&Path], + timeout: Duration, +) -> Result { + let mut cmd = Command::new(command); + cmd.args(args); + for arg in path_args { + cmd.arg(arg); + } + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + let mut child = cmd + .spawn() + .map_err(|err| format!("failed to run {}: {err}", command.display()))?; + let mut stdout = child.stdout.take().expect("stdout piped"); + let mut stderr = child.stderr.take().expect("stderr piped"); + let stdout_reader = thread::spawn(move || { + let mut buffer = Vec::new(); + let _ = stdout.read_to_end(&mut buffer); + buffer + }); + let stderr_reader = thread::spawn(move || { + let mut buffer = Vec::new(); + let _ = stderr.read_to_end(&mut buffer); + buffer + }); + + let start = Instant::now(); + let status = loop { + if let Some(status) = child + .try_wait() + .map_err(|err| format!("failed to poll {}: {err}", command.display()))? + { + break status; + } + if start.elapsed() >= timeout { + let _ = child.kill(); + let _ = child.wait(); + let stdout = stdout_reader.join().unwrap_or_default(); + let stderr = stderr_reader.join().unwrap_or_default(); + return Err(format!( + "{} timed out after {:?}\nstdout:\n{}\nstderr:\n{}", + command.display(), + timeout, + String::from_utf8_lossy(&stdout), + String::from_utf8_lossy(&stderr) + )); + } + thread::sleep(Duration::from_millis(20)); + }; + + let stdout = stdout_reader.join().unwrap_or_default(); + let stderr = stderr_reader.join().unwrap_or_default(); + Ok(CommandOutput { + status, + stdout, + stderr, + }) +} + +pub fn command_available(command: &Path) -> bool { + run_command(command, &["--version"], &[], Duration::from_secs(10)) + .is_ok_and(|output| output.status.success()) +} + +pub fn foundry_tool_path(env_var: &str, tool: &str) -> PathBuf { + if let Some(path) = env::var_os(env_var) { + return PathBuf::from(path); + } + if let Some(home) = env::var_os("HOME") { + let foundry = PathBuf::from(home).join(".foundry/bin").join(tool); + if foundry.exists() { + return foundry; + } + } + PathBuf::from(tool) +} + +struct Anvil { + child: Child, + url: String, + logs: Arc>, + readers: Vec>, +} + +impl Anvil { + fn spawn(anvil: &Path, cast: &Path) -> Result { + // Sonatina currently targets Osaka and may emit Osaka-only opcodes. + // Keep the runtime target aligned unless a caller explicitly overrides it. + let hardfork = env::var_os("ANVIL_HARDFORK").unwrap_or_else(|| "osaka".into()); + let mut child = Command::new(anvil) + .arg("--host") + .arg("127.0.0.1") + .arg("--port") + .arg("0") + .arg("--hardfork") + .arg(hardfork) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| format!("failed to start {}: {err}", anvil.display()))?; + + let logs = Arc::new(Mutex::new(String::new())); + let (sender, receiver) = mpsc::channel(); + let mut readers = Vec::new(); + if let Some(stdout) = child.stdout.take() { + readers.push(spawn_log_reader(stdout, logs.clone(), sender.clone())); + } + if let Some(stderr) = child.stderr.take() { + readers.push(spawn_log_reader(stderr, logs.clone(), sender)); + } + + let port = match wait_for_anvil_port(&mut child, &receiver, &logs) { + Ok(port) => port, + Err(message) => { + let _ = child.kill(); + let _ = child.wait(); + for reader in readers { + let _ = reader.join(); + } + return Err(message); + } + }; + let anvil = Self { + child, + url: format!("http://127.0.0.1:{port}"), + logs, + readers, + }; + anvil.wait_until_ready(cast)?; + Ok(anvil) + } + + fn url(&self) -> &str { + &self.url + } + + fn logs(&self) -> String { + self.logs.lock().expect("anvil logs lock").clone() + } + + fn wait_until_ready(&self, cast: &Path) -> Result<(), String> { + let start = Instant::now(); + while start.elapsed() < ANVIL_READY_TIMEOUT { + let output = run_command( + cast, + &["block-number", "--rpc-url", &self.url], + &[], + Duration::from_secs(2), + ); + if output.is_ok_and(|output| output.status.success()) { + return Ok(()); + } + thread::sleep(Duration::from_millis(100)); + } + Err(format!( + "anvil did not become ready at {}\nlogs:\n{}", + self.url, + self.logs() + )) + } +} + +impl Drop for Anvil { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + for reader in self.readers.drain(..) { + let _ = reader.join(); + } + } +} + +fn spawn_log_reader( + reader: R, + logs: Arc>, + sender: mpsc::Sender, +) -> thread::JoinHandle<()> { + thread::spawn(move || { + let reader = BufReader::new(reader); + for line in reader.lines().map_while(Result::ok) { + { + let mut logs = logs.lock().expect("anvil logs lock"); + logs.push_str(&line); + logs.push('\n'); + } + let _ = sender.send(line); + } + }) +} + +fn wait_for_anvil_port( + child: &mut Child, + receiver: &mpsc::Receiver, + logs: &Arc>, +) -> Result { + let start = Instant::now(); + while start.elapsed() < ANVIL_START_TIMEOUT { + if let Some(status) = child + .try_wait() + .map_err(|err| format!("failed to poll anvil: {err}"))? + { + return Err(format!( + "anvil exited before printing a port: {status}\nlogs:\n{}", + logs.lock().expect("anvil logs lock") + )); + } + match receiver.recv_timeout(Duration::from_millis(100)) { + Ok(line) => { + if let Some(port) = parse_anvil_port(&line) { + return Ok(port); + } + } + Err(mpsc::RecvTimeoutError::Timeout) => {} + Err(mpsc::RecvTimeoutError::Disconnected) => break, + } + } + Err(format!( + "anvil did not print a listening port\nlogs:\n{}", + logs.lock().expect("anvil logs lock") + )) +} + +fn parse_anvil_port(line: &str) -> Option { + for marker in ["127.0.0.1:", "localhost:"] { + let Some(start) = line.find(marker).map(|index| index + marker.len()) else { + continue; + }; + let digits = line[start..] + .chars() + .take_while(|character| character.is_ascii_digit()) + .collect::(); + if let Ok(port) = digits.parse() { + return Some(port); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exact_returndata_preserves_full_width_words() { + let expected = [0xff; 32]; + assert_return_data( + "uint256 max", + &expected, + &format!("0x{}", encode_hex(&expected)), + ) + .expect("matching full-width returndata"); + assert_eq!(decode_hex_data("0x").unwrap(), Vec::::new()); + assert_eq!( + assert_return_data("mismatch", &[1], "0x02") + .unwrap_err() + .kind, + FailureKind::Mismatch + ); + } + + #[test] + fn parses_anvil_listening_addresses() { + assert_eq!(parse_anvil_port("Listening on 127.0.0.1:8545"), Some(8545)); + assert_eq!(parse_anvil_port("http://localhost:49152"), Some(49152)); + assert_eq!(parse_anvil_port("unrelated"), None); + } + + #[test] + fn requires_successful_transaction_receipts() { + require_successful_transaction_receipt(br#"{"transactionHash":"0xabc","status":"0x1"}"#) + .expect("hex success status"); + require_successful_transaction_receipt(br#"{"transactionHash":"0xabc","status":1}"#) + .expect("numeric success status"); + + let reverted = require_successful_transaction_receipt( + br#"{"transactionHash":"0xabc","status":"0x0"}"#, + ) + .unwrap_err(); + assert_eq!(reverted.kind, FailureKind::Transaction); + assert!(reverted.message.contains("not successful"), "{reverted}"); + + let missing = + require_successful_transaction_receipt(br#"{"transactionHash":"0xabc"}"#).unwrap_err(); + assert!(missing.message.contains("no status"), "{missing}"); + } + + #[test] + fn decodes_json_rpc_reverts_and_checks_payloads() { + let response = br#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted","data":"0xdeadbeef"}}"#; + let outcome = decode_eth_call_response(response).expect("revert response"); + assert_eq!( + outcome, + CallOutcome::Revert(Some(vec![0xde, 0xad, 0xbe, 0xef])) + ); + assert_call_outcome( + "exact revert", + &ResolvedExpectedOutcome::Revert(Some(vec![0xde, 0xad, 0xbe, 0xef])), + &outcome, + ) + .expect("matching revert"); + assert_eq!( + assert_call_outcome( + "wrong payload", + &ResolvedExpectedOutcome::Revert(Some(vec![0xca, 0xfe])), + &outcome, + ) + .unwrap_err() + .kind, + FailureKind::Mismatch + ); + assert_eq!( + assert_call_outcome( + "unexpected success", + &ResolvedExpectedOutcome::Revert(None), + &CallOutcome::Return("0x".to_owned()), + ) + .unwrap_err() + .kind, + FailureKind::Mismatch + ); + + let non_revert = br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"invalid params","data":"0xdeadbeef"}}"#; + let error = decode_eth_call_response(non_revert).unwrap_err(); + assert_eq!(error.kind, FailureKind::Call); + assert!(error.message.contains("invalid params"), "{error}"); + + let conflicting = br#"{"jsonrpc":"2.0","id":1,"result":"0x","error":{"code":3,"message":"execution reverted"}}"#; + assert!( + decode_eth_call_response(conflicting) + .unwrap_err() + .message + .contains("exactly one") + ); + } + + #[test] + fn validates_anvil_http_response_framing() { + let body = br#"{"jsonrpc":"2.0","id":1,"result":"0x"}"#; + let response = [ + format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n", + body.len() + ) + .into_bytes(), + body.to_vec(), + ] + .concat(); + assert_eq!(parse_http_response(response).unwrap(), body); + + let truncated = b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n{}".to_vec(); + assert!( + parse_http_response(truncated) + .unwrap_err() + .message + .contains("truncated") + ); + let chunked = + b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\n{}\r\n0\r\n\r\n".to_vec(); + assert!( + parse_http_response(chunked) + .unwrap_err() + .message + .contains("transfer encoding") + ); + } +} diff --git a/crates/test-utils/src/e2e/directive.rs b/crates/test-utils/src/e2e/directive.rs new file mode 100644 index 00000000..0906c0b5 --- /dev/null +++ b/crates/test-utils/src/e2e/directive.rs @@ -0,0 +1,1045 @@ +//! Parser and static-ABI resolver for source-embedded E2E directives. + +use std::{fmt, str::FromStr}; + +use super::{E2eFailure, FailureKind, encode_hex}; + +/// One unsigned EVM word in big-endian byte order. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Word256([u8; 32]); + +impl Word256 { + pub const ZERO: Self = Self([0; 32]); + + pub const fn from_be_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + pub const fn from_u128(value: u128) -> Self { + let mut bytes = [0; 32]; + let value = value.to_be_bytes(); + let mut index = 0; + while index < value.len() { + bytes[16 + index] = value[index]; + index += 1; + } + Self(bytes) + } + + pub const fn as_be_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub const fn into_be_bytes(self) -> [u8; 32] { + self.0 + } + + pub fn fits_bits(self, bits: usize) -> bool { + if bits >= 256 { + return true; + } + let full_zero_bytes = (256 - bits) / 8; + if self.0[..full_zero_bytes].iter().any(|byte| *byte != 0) { + return false; + } + let remaining_high_bits = (256 - bits) % 8; + remaining_high_bits == 0 + || self.0[full_zero_bytes] & (0xff << (8 - remaining_high_bits)) == 0 + } + + pub fn to_hex(self) -> String { + encode_hex(&self.0) + } + + fn parse_hex(literal: &str) -> Result { + let digits = literal + .strip_prefix("0x") + .or_else(|| literal.strip_prefix("0X")) + .ok_or_else(|| Word256ParseError::new("hexadecimal word must start with `0x`"))?; + if digits.is_empty() { + return Err(Word256ParseError::new( + "hexadecimal word requires at least one digit", + )); + } + if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(Word256ParseError::new( + "hexadecimal word contains a non-hex digit", + )); + } + if digits.len() > 64 { + return Err(Word256ParseError::new("value does not fit uint256")); + } + + let mut bytes = [0; 32]; + let mut source = digits.len(); + let mut destination = bytes.len(); + while source > 0 { + let low = hex_nibble(digits.as_bytes()[source - 1]); + source -= 1; + let high = if source > 0 { + let nibble = hex_nibble(digits.as_bytes()[source - 1]); + source -= 1; + nibble + } else { + 0 + }; + destination -= 1; + bytes[destination] = (high << 4) | low; + } + Ok(Self(bytes)) + } + + fn parse_decimal(literal: &str) -> Result { + if literal.is_empty() { + return Err(Word256ParseError::new( + "decimal word requires at least one digit", + )); + } + if !literal.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(Word256ParseError::new( + "decimal word contains a non-decimal digit", + )); + } + + let mut bytes = [0u8; 32]; + for digit in literal.bytes().map(|byte| byte - b'0') { + let mut carry = u16::from(digit); + for byte in bytes.iter_mut().rev() { + let next = u16::from(*byte) * 10 + carry; + *byte = next as u8; + carry = next >> 8; + } + if carry != 0 { + return Err(Word256ParseError::new("value does not fit uint256")); + } + } + Ok(Self(bytes)) + } +} + +fn hex_nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => unreachable!("hex input was validated"), + } +} + +impl FromStr for Word256 { + type Err = Word256ParseError; + + fn from_str(literal: &str) -> Result { + if literal.starts_with("0x") || literal.starts_with("0X") { + Self::parse_hex(literal) + } else { + Self::parse_decimal(literal) + } + } +} + +impl From for Word256 { + fn from(value: u128) -> Self { + Self::from_u128(value) + } +} + +impl fmt::Debug for Word256 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "Word256(0x{})", self.to_hex()) + } +} + +impl fmt::Display for Word256 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "0x{}", self.to_hex()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Word256ParseError { + message: String, +} + +impl Word256ParseError { + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl fmt::Display for Word256ParseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for Word256ParseError {} + +/// A literal accepted by an E2E directive. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DirectiveValue { + Word(Word256), + Bool(bool), + Tuple(Vec), +} + +/// Expected result of one directive call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExpectedOutcome { + Return(Vec), + /// Any revert when the payload is `None`, or an exact payload otherwise. + Revert(Option>), +} + +/// Action requested by one source-comment directive. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum E2eAction { + /// Execute with `eth_call` and compare the returned or reverted data. + Call(ExpectedOutcome), + /// Submit a state-changing transaction and require a successful receipt. + Send, +} + +/// Parsed contents of one `#[...]` source-comment directive. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct E2eDirective { + pub args: Vec, + pub action: E2eAction, +} + +/// A backend-neutral, static external-ABI shape. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AbiShape { + Word, + Bool, + Address, + Bytes32, + Unit, + Tuple(Vec), + Unsupported(String), +} + +impl fmt::Display for AbiShape { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Word => formatter.write_str("uint256"), + Self::Bool => formatter.write_str("bool"), + Self::Address => formatter.write_str("address"), + Self::Bytes32 => formatter.write_str("bytes32"), + Self::Unit => formatter.write_str("()"), + Self::Tuple(elements) => { + formatter.write_str("(")?; + for (index, element) in elements.iter().enumerate() { + if index > 0 { + formatter.write_str(", ")?; + } + write!(formatter, "{element}")?; + } + formatter.write_str(")") + } + Self::Unsupported(name) => write!(formatter, "unsupported ABI type `{name}`"), + } + } +} + +/// ABI-encoded form of a parsed directive, ready for an EVM call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedE2eCall { + pub signature: String, + pub selector: [u8; 4], + /// Selector plus ABI-encoded arguments, with a `0x` prefix. + pub calldata: String, + pub action: ResolvedE2eAction, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolvedExpectedOutcome { + Return(Vec), + Revert(Option>), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolvedE2eAction { + Call(ResolvedExpectedOutcome), + Send, +} + +impl ResolvedE2eCall { + pub fn expected_return_data(&self) -> Option<&[u8]> { + match &self.action { + ResolvedE2eAction::Call(ResolvedExpectedOutcome::Return(data)) => Some(data), + ResolvedE2eAction::Call(ResolvedExpectedOutcome::Revert(_)) + | ResolvedE2eAction::Send => None, + } + } +} + +/// Syntax or ABI-resolution error for an E2E directive. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirectiveError { + /// Byte offset within the trimmed directive comment for syntax errors. + pub offset: Option, + pub message: String, +} + +impl DirectiveError { + fn syntax(offset: usize, message: impl Into) -> Self { + Self { + offset: Some(offset), + message: message.into(), + } + } + + fn semantic(message: impl Into) -> Self { + Self { + offset: None, + message: message.into(), + } + } +} + +impl fmt::Display for DirectiveError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(offset) = self.offset { + write!(formatter, "{} at byte {offset}", self.message) + } else { + formatter.write_str(&self.message) + } + } +} + +impl std::error::Error for DirectiveError {} + +impl From for E2eFailure { + fn from(error: DirectiveError) -> Self { + Self::new(FailureKind::Directive, error.to_string()) + } +} + +/// Parses a source comment as an E2E directive. +/// +/// Ordinary comments return `Ok(None)`. Once the trimmed comment starts with +/// `#[`, the complete comment must conform to the directive grammar; malformed +/// directives are never silently ignored. Both delimiter-free HIR comment +/// text and raw `//` / `/* ... */` text are accepted. +pub fn parse_e2e_directive(comment: &str) -> Result, DirectiveError> { + let comment = comment_text(comment); + if !comment.starts_with("#[") { + return Ok(None); + } + DirectiveParser::new(comment).parse().map(Some) +} + +fn comment_text(comment: &str) -> &str { + let comment = comment.trim(); + if let Some(line) = comment.strip_prefix("//") { + return line.trim(); + } + if let Some(block) = comment + .strip_prefix("/*") + .and_then(|comment| comment.strip_suffix("*/")) + { + return block.trim(); + } + comment +} + +/// Type-checks and ABI-encodes a source directive for a concrete method. +pub fn resolve_e2e_directive( + signature: impl Into, + selector: [u8; 4], + inputs: &[AbiShape], + outputs: &[AbiShape], + directive: &E2eDirective, +) -> Result { + let signature = signature.into(); + let arguments = encode_abi_values("argument", inputs, &directive.args) + .map_err(|error| DirectiveError::semantic(format!("{signature}: {}", error.message)))?; + let action = match &directive.action { + E2eAction::Call(ExpectedOutcome::Return(values)) => { + let bytes = encode_abi_values("result", outputs, values).map_err(|error| { + DirectiveError::semantic(format!("{signature}: {}", error.message)) + })?; + ResolvedE2eAction::Call(ResolvedExpectedOutcome::Return(bytes)) + } + E2eAction::Call(ExpectedOutcome::Revert(payload)) => { + ResolvedE2eAction::Call(ResolvedExpectedOutcome::Revert(payload.clone())) + } + E2eAction::Send => ResolvedE2eAction::Send, + }; + + let mut calldata = Vec::with_capacity(4 + arguments.len()); + calldata.extend_from_slice(&selector); + calldata.extend_from_slice(&arguments); + Ok(ResolvedE2eCall { + signature, + selector, + calldata: format!("0x{}", encode_hex(&calldata)), + action, + }) +} + +/// Extracts and resolves every E2E directive from a method's HIR comments. +/// +/// Normal comments are ignored. A malformed `#[...]` comment is reported with +/// the method signature, as are ABI arity and type mismatches. +pub fn resolve_e2e_comments<'comment>( + signature: impl Into, + selector: [u8; 4], + inputs: &[AbiShape], + outputs: &[AbiShape], + comments: impl IntoIterator, +) -> Result, E2eFailure> { + let signature = signature.into(); + let mut calls = Vec::new(); + for comment in comments { + let directive = parse_e2e_directive(comment).map_err(|error| { + E2eFailure::new(FailureKind::Directive, format!("{signature}: {error}")) + })?; + let Some(directive) = directive else { + continue; + }; + calls.push( + resolve_e2e_directive(signature.clone(), selector, inputs, outputs, &directive) + .map_err(E2eFailure::from)?, + ); + } + Ok(calls) +} + +/// Encodes static ABI values after checking their directive shape. +pub fn encode_static_abi( + label: &str, + shapes: &[AbiShape], + values: &[DirectiveValue], +) -> Result, DirectiveError> { + encode_abi_values(label, shapes, values) +} + +fn encode_abi_values( + label: &str, + shapes: &[AbiShape], + values: &[DirectiveValue], +) -> Result, DirectiveError> { + if shapes.len() != values.len() { + return Err(DirectiveError::semantic(format!( + "expected {} ABI {label}{}, directive provides {}", + shapes.len(), + if shapes.len() == 1 { "" } else { "s" }, + values.len() + ))); + } + + let mut encoded = Vec::new(); + for (index, (shape, value)) in shapes.iter().zip(values).enumerate() { + encode_abi_value( + &format!("{label} {}", index + 1), + shape, + value, + &mut encoded, + )?; + } + Ok(encoded) +} + +fn encode_abi_value( + path: &str, + shape: &AbiShape, + value: &DirectiveValue, + encoded: &mut Vec, +) -> Result<(), DirectiveError> { + match (shape, value) { + (AbiShape::Word | AbiShape::Bytes32, DirectiveValue::Word(word)) => { + encoded.extend_from_slice(word.as_be_bytes()); + Ok(()) + } + (AbiShape::Address, DirectiveValue::Word(word)) => { + if !word.fits_bits(160) { + return Err(DirectiveError::semantic(format!( + "{path}: address value {word} exceeds 160 bits" + ))); + } + encoded.extend_from_slice(word.as_be_bytes()); + Ok(()) + } + (AbiShape::Bool, DirectiveValue::Bool(value)) => { + let mut word = [0; 32]; + word[31] = u8::from(*value); + encoded.extend_from_slice(&word); + Ok(()) + } + (AbiShape::Unit, DirectiveValue::Tuple(values)) if values.is_empty() => Ok(()), + (AbiShape::Tuple(shapes), DirectiveValue::Tuple(values)) => { + let nested = encode_tuple_values(path, shapes, values)?; + encoded.extend_from_slice(&nested); + Ok(()) + } + (AbiShape::Unsupported(name), _) => Err(DirectiveError::semantic(format!( + "{path}: unsupported ABI type `{name}`" + ))), + _ => Err(DirectiveError::semantic(format!( + "{path}: expected {shape}, found {}", + directive_value_kind(value) + ))), + } +} + +fn encode_tuple_values( + path: &str, + shapes: &[AbiShape], + values: &[DirectiveValue], +) -> Result, DirectiveError> { + if shapes.len() != values.len() { + return Err(DirectiveError::semantic(format!( + "{path}: expected {} tuple element{}, directive provides {}", + shapes.len(), + if shapes.len() == 1 { "" } else { "s" }, + values.len() + ))); + } + + let mut encoded = Vec::new(); + for (index, (shape, value)) in shapes.iter().zip(values).enumerate() { + encode_abi_value( + &format!("{path}, tuple element {}", index + 1), + shape, + value, + &mut encoded, + )?; + } + Ok(encoded) +} + +fn directive_value_kind(value: &DirectiveValue) -> &'static str { + match value { + DirectiveValue::Word(_) => "uint256 literal", + DirectiveValue::Bool(_) => "boolean literal", + DirectiveValue::Tuple(_) => "tuple literal", + } +} + +struct DirectiveParser<'source> { + source: &'source str, + position: usize, +} + +impl<'source> DirectiveParser<'source> { + fn new(source: &'source str) -> Self { + Self { + source, + position: 0, + } + } + + fn parse(mut self) -> Result { + self.expect("#[")?; + let send = self.consume_keyword("send"); + let args = self.parse_value_list()?; + let action = if send { + E2eAction::Send + } else { + self.expect("->")?; + E2eAction::Call(self.parse_expected()?) + }; + self.expect("]")?; + self.skip_whitespace(); + if !self.is_eof() { + return Err(self.error("unexpected trailing text after directive")); + } + Ok(E2eDirective { args, action }) + } + + fn parse_expected(&mut self) -> Result { + self.skip_whitespace(); + if self.consume_keyword("revert") { + self.skip_whitespace(); + let payload = if self.consume("(") { + self.skip_whitespace(); + let literal = self.take_while(|byte| byte.is_ascii_hexdigit() || byte == b'x'); + if literal.is_empty() { + return Err(self.error("expected a `0x` revert payload")); + } + let bytes = parse_hex_bytes(literal).map_err(|message| self.error(message))?; + self.skip_whitespace(); + self.expect(")")?; + Some(bytes) + } else { + None + }; + return Ok(ExpectedOutcome::Revert(payload)); + } + if self.peek("(") { + return self.parse_value_list().map(ExpectedOutcome::Return); + } + self.parse_value() + .map(|value| ExpectedOutcome::Return(vec![value])) + } + + fn parse_value_list(&mut self) -> Result, DirectiveError> { + self.expect("(")?; + self.skip_whitespace(); + if self.consume(")") { + return Ok(Vec::new()); + } + + let mut values = Vec::new(); + loop { + values.push(self.parse_value()?); + self.skip_whitespace(); + if self.consume(")") { + return Ok(values); + } + self.expect(",")?; + } + } + + fn parse_value(&mut self) -> Result { + self.skip_whitespace(); + if self.consume_keyword("true") { + return Ok(DirectiveValue::Bool(true)); + } + if self.consume_keyword("false") { + return Ok(DirectiveValue::Bool(false)); + } + if self.peek("(") { + return self.parse_value_list().map(DirectiveValue::Tuple); + } + + let start = self.position; + let literal = if self.peek("0x") || self.peek("0X") { + self.position += 2; + self.take_while(|byte| byte.is_ascii_hexdigit()); + &self.source[start..self.position] + } else { + self.take_while(|byte| byte.is_ascii_digit()) + }; + if literal.is_empty() { + return Err(self.error("expected uint256, boolean, or tuple value")); + } + let word = literal + .parse::() + .map_err(|error| self.error(error.to_string()))?; + Ok(DirectiveValue::Word(word)) + } + + fn expect(&mut self, expected: &str) -> Result<(), DirectiveError> { + self.skip_whitespace(); + if self.consume(expected) { + Ok(()) + } else { + Err(self.error(format!("expected `{expected}`"))) + } + } + + fn consume_keyword(&mut self, keyword: &str) -> bool { + self.skip_whitespace(); + if !self.peek(keyword) { + return false; + } + let end = self.position + keyword.len(); + if self + .source + .as_bytes() + .get(end) + .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_') + { + return false; + } + self.position = end; + true + } + + fn consume(&mut self, expected: &str) -> bool { + if !self.peek(expected) { + return false; + } + self.position += expected.len(); + true + } + + fn peek(&self, expected: &str) -> bool { + self.source[self.position..].starts_with(expected) + } + + fn take_while(&mut self, predicate: impl Fn(u8) -> bool) -> &'source str { + let start = self.position; + while self + .source + .as_bytes() + .get(self.position) + .copied() + .is_some_and(&predicate) + { + self.position += 1; + } + &self.source[start..self.position] + } + + fn skip_whitespace(&mut self) { + while self + .source + .as_bytes() + .get(self.position) + .is_some_and(u8::is_ascii_whitespace) + { + self.position += 1; + } + } + + fn is_eof(&self) -> bool { + self.position == self.source.len() + } + + fn error(&self, message: impl Into) -> DirectiveError { + DirectiveError::syntax(self.position, message) + } +} + +fn parse_hex_bytes(literal: &str) -> Result, String> { + let Some(digits) = literal + .strip_prefix("0x") + .or_else(|| literal.strip_prefix("0X")) + else { + return Err("revert payload must start with `0x`".to_owned()); + }; + if !digits.len().is_multiple_of(2) { + return Err("revert payload must contain a whole number of bytes".to_owned()); + } + if !digits.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err("revert payload is not hexadecimal".to_owned()); + } + digits + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).expect("ASCII hex is UTF-8"); + u8::from_str_radix(pair, 16).map_err(|error| error.to_string()) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn word(value: u128) -> DirectiveValue { + DirectiveValue::Word(value.into()) + } + + fn resolution_error(source: &str, inputs: &[AbiShape], outputs: &[AbiShape]) -> DirectiveError { + let directive = parse_e2e_directive(source).unwrap().unwrap(); + resolve_e2e_directive("f", [0; 4], inputs, outputs, &directive).unwrap_err() + } + + #[test] + fn parses_scalar_and_multi_output_directives() { + assert_eq!( + parse_e2e_directive(" // #[(0, 1) -> 1] ").unwrap(), + Some(E2eDirective { + args: vec![word(0), word(1)], + action: E2eAction::Call(ExpectedOutcome::Return(vec![word(1)])), + }) + ); + assert_eq!( + parse_e2e_directive("#[((1, 2), true) -> (3, false)]").unwrap(), + Some(E2eDirective { + args: vec![ + DirectiveValue::Tuple(vec![word(1), word(2)]), + DirectiveValue::Bool(true) + ], + action: E2eAction::Call(ExpectedOutcome::Return(vec![ + word(3), + DirectiveValue::Bool(false), + ])), + }) + ); + assert_eq!( + parse_e2e_directive("#[() -> ()]").unwrap(), + Some(E2eDirective { + args: Vec::new(), + action: E2eAction::Call(ExpectedOutcome::Return(Vec::new())), + }) + ); + } + + #[test] + fn parses_state_changing_send_directives() { + assert_eq!( + parse_e2e_directive("#[send(1, true)]").unwrap(), + Some(E2eDirective { + args: vec![word(1), DirectiveValue::Bool(true)], + action: E2eAction::Send, + }) + ); + let error = parse_e2e_directive("#[send(1) -> ()]").unwrap_err(); + assert!(error.message.contains("expected `]`"), "{error}"); + + let directive = parse_e2e_directive("#[send(7)]").unwrap().unwrap(); + let resolved = resolve_e2e_directive( + "set(uint256)", + [0x12, 0x34, 0x56, 0x78], + &[AbiShape::Word], + &[AbiShape::Word], + &directive, + ) + .unwrap(); + assert_eq!(resolved.action, ResolvedE2eAction::Send); + assert_eq!( + resolved.calldata, + format!("0x12345678{}", Word256::from_u128(7).to_hex()) + ); + } + + #[test] + fn ignores_normal_comments_but_rejects_malformed_directives() { + assert_eq!(parse_e2e_directive("ordinary comment").unwrap(), None); + assert_eq!( + parse_e2e_directive("// mentions #[()] later").unwrap(), + None + ); + let error = parse_e2e_directive("#[(1, 2)] -> 3)").unwrap_err(); + assert!(error.message.contains("expected `->`"), "{error}"); + let error = parse_e2e_directive("#[(1) -> 1] trailing").unwrap_err(); + assert!(error.message.contains("trailing text"), "{error}"); + } + + #[test] + fn parses_full_width_words_and_rejects_overflow() { + let max_hex = format!("0x{}", "f".repeat(64)); + let max_decimal = + "115792089237316195423570985008687907853269984665640564039457584007913129639935"; + assert_eq!( + max_hex.parse::().unwrap(), + max_decimal.parse::().unwrap() + ); + assert!(format!("0x1{}", "0".repeat(64)).parse::().is_err()); + assert!( + "115792089237316195423570985008687907853269984665640564039457584007913129639936" + .parse::() + .is_err() + ); + } + + #[test] + fn resolves_static_abi_calldata_and_expected_returndata() { + let directive = parse_e2e_directive("#[(42, true, (1, 2)) -> (true, 3)]") + .unwrap() + .unwrap(); + let resolved = resolve_e2e_directive( + "f(uint256,bool,(uint256,uint256))", + [0x12, 0x34, 0x56, 0x78], + &[ + AbiShape::Word, + AbiShape::Bool, + AbiShape::Tuple(vec![AbiShape::Word, AbiShape::Word]), + ], + &[AbiShape::Bool, AbiShape::Word], + &directive, + ) + .unwrap(); + assert_eq!( + resolved.calldata, + format!( + "0x12345678{}{}{}{}", + Word256::from_u128(42).to_hex(), + Word256::from_u128(1).to_hex(), + Word256::from_u128(1).to_hex(), + Word256::from_u128(2).to_hex(), + ) + ); + assert_eq!( + resolved.expected_return_data(), + Some( + [ + Word256::from_u128(1).as_be_bytes().as_slice(), + Word256::from_u128(3).as_be_bytes().as_slice(), + ] + .concat() + .as_slice() + ) + ); + } + + #[test] + fn reports_abi_shape_errors() { + let directive = parse_e2e_directive("#[(1) -> true]").unwrap().unwrap(); + let error = resolve_e2e_directive( + "f(bool)", + [0; 4], + &[AbiShape::Bool], + &[AbiShape::Bool], + &directive, + ) + .unwrap_err(); + assert!(error.message.contains("expected bool"), "{error}"); + + let address = + DirectiveValue::Word(format!("0x1{}", "0".repeat(40)).parse::().unwrap()); + let error = encode_static_abi("argument", &[AbiShape::Address], &[address]).unwrap_err(); + assert!(error.message.contains("exceeds 160 bits"), "{error}"); + } + + #[test] + fn reports_top_level_input_and_result_shape_mismatches() { + let error = resolution_error("#[(1) -> 1]", &[AbiShape::Bool], &[AbiShape::Word]); + assert_eq!( + error.message, + "f: argument 1: expected bool, found uint256 literal" + ); + + let error = resolution_error("#[(1) -> 1]", &[AbiShape::Word], &[AbiShape::Bool]); + assert_eq!( + error.message, + "f: result 1: expected bool, found uint256 literal" + ); + + let tuple = AbiShape::Tuple(vec![AbiShape::Word, AbiShape::Bool]); + let error = resolution_error( + "#[(1) -> 1]", + std::slice::from_ref(&tuple), + &[AbiShape::Word], + ); + assert_eq!( + error.message, + "f: argument 1: expected (uint256, bool), found uint256 literal" + ); + + let error = resolution_error("#[(1) -> 1]", &[AbiShape::Word], &[tuple]); + assert_eq!( + error.message, + "f: result 1: expected (uint256, bool), found uint256 literal" + ); + } + + #[test] + fn reports_top_level_input_and_result_arity_mismatches() { + let error = resolution_error( + "#[(1) -> 1]", + &[AbiShape::Word, AbiShape::Word], + &[AbiShape::Word], + ); + assert_eq!( + error.message, + "f: expected 2 ABI arguments, directive provides 1" + ); + + let error = resolution_error("#[(1) -> (1, 2)]", &[AbiShape::Word], &[AbiShape::Word]); + assert_eq!( + error.message, + "f: expected 1 ABI result, directive provides 2" + ); + } + + #[test] + fn reports_nested_input_tuple_paths_and_arities() { + let nested = AbiShape::Tuple(vec![ + AbiShape::Word, + AbiShape::Tuple(vec![AbiShape::Bool, AbiShape::Word]), + ]); + let error = resolution_error( + "#[((1, (true, false))) -> 1]", + std::slice::from_ref(&nested), + &[AbiShape::Word], + ); + assert_eq!( + error.message, + "f: argument 1, tuple element 2, tuple element 2: expected uint256, found boolean literal" + ); + + let error = resolution_error( + "#[((1)) -> 1]", + &[AbiShape::Tuple(vec![AbiShape::Word, AbiShape::Bool])], + &[AbiShape::Word], + ); + assert_eq!( + error.message, + "f: argument 1: expected 2 tuple elements, directive provides 1" + ); + + let error = resolution_error("#[((1, (true))) -> 1]", &[nested], &[AbiShape::Word]); + assert_eq!( + error.message, + "f: argument 1, tuple element 2: expected 2 tuple elements, directive provides 1" + ); + } + + #[test] + fn reports_nested_result_tuple_paths_and_arities() { + let nested = AbiShape::Tuple(vec![ + AbiShape::Word, + AbiShape::Tuple(vec![AbiShape::Bool, AbiShape::Word]), + ]); + let error = resolution_error( + "#[() -> ((1, (true, false)))]", + &[], + std::slice::from_ref(&nested), + ); + assert_eq!( + error.message, + "f: result 1, tuple element 2, tuple element 2: expected uint256, found boolean literal" + ); + + let error = resolution_error( + "#[() -> ((1))]", + &[], + &[AbiShape::Tuple(vec![AbiShape::Word, AbiShape::Bool])], + ); + assert_eq!( + error.message, + "f: result 1: expected 2 tuple elements, directive provides 1" + ); + + let error = resolution_error("#[() -> ((1, (true)))]", &[], &[nested]); + assert_eq!( + error.message, + "f: result 1, tuple element 2: expected 2 tuple elements, directive provides 1" + ); + } + + #[test] + fn resolves_only_directive_comments_and_labels_errors() { + let comments = ["ordinary note", "#[(1, 2) -> 3]", "another note"]; + let calls = resolve_e2e_comments( + "add(uint256,uint256)", + [1, 2, 3, 4], + &[AbiShape::Word, AbiShape::Word], + &[AbiShape::Word], + comments, + ) + .unwrap(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].signature, "add(uint256,uint256)"); + + let error = resolve_e2e_comments( + "add(uint256,uint256)", + [1, 2, 3, 4], + &[AbiShape::Word, AbiShape::Word], + &[AbiShape::Word], + ["#[(1)] -> 2)"], + ) + .unwrap_err(); + assert!(error.message.contains("add(uint256,uint256)"), "{error}"); + } + + #[test] + fn parses_revert_expectations() { + assert_eq!( + parse_e2e_directive("#[() -> revert]").unwrap(), + Some(E2eDirective { + args: Vec::new(), + action: E2eAction::Call(ExpectedOutcome::Revert(None)), + }) + ); + assert_eq!( + parse_e2e_directive("#[() -> revert(0xdeadbeef)]").unwrap(), + Some(E2eDirective { + args: Vec::new(), + action: E2eAction::Call(ExpectedOutcome::Revert(Some(vec![ + 0xde, 0xad, 0xbe, 0xef, + ]))), + }) + ); + } +} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 00000000..e6cee3b0 --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,528 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, panic, + path::{Path, PathBuf}, + thread, +}; + +use annotate_snippets::Renderer; +use hir::{ + diag::{AnyDiagnostic, Diagnostic, sort_dedup_rendered_diagnostics}, + input::SourceFile, +}; +use nameres::{ + LibraryId, ModuleFsSnapshot, ModuleKey, ModuleTree, module_id_from_key, module_key_for_path, + resolve_module_path_candidate, +}; +use rustc_hash::FxHashSet; +use url::Url; + +pub mod e2e; + +pub mod reexports { + pub use hir; + pub use nameres; + pub use parser; + pub use rustc_hash; + pub use salsa; +} + +pub trait FrontendTestDb: hir::Db + parser::Db + nameres::Db + Sized { + fn set_module_tree(&mut self, tree: ModuleTree); + fn set_module_fs_snapshot(&mut self, snapshot: ModuleFsSnapshot); + fn insert_module_file(&mut self, key: ModuleKey, file: SourceFile); + fn contains_module_file(&self, key: &ModuleKey) -> bool; + fn module_file_for_key(&self, key: &ModuleKey) -> Option; +} + +#[macro_export] +macro_rules! define_frontend_test_db { + ($name:ident, $typeck_crate:ident) => { + #[salsa::db] + #[derive(Clone)] + struct $name { + storage: $crate::reexports::salsa::Storage, + module_tree: Option<$crate::reexports::nameres::ModuleTree>, + module_fs_snapshot: Option<$crate::reexports::nameres::ModuleFsSnapshot>, + module_file_snapshot: Option<$crate::reexports::nameres::ModuleFileSnapshot>, + } + + impl Default for $name { + fn default() -> Self { + let mut db = Self { + storage: $crate::reexports::salsa::Storage::default(), + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + }; + db.module_tree = Some($crate::reexports::nameres::ModuleTree::new( + &db, + std::path::PathBuf::from("/main"), + std::path::PathBuf::from("/std"), + std::collections::BTreeMap::new(), + )); + db.module_fs_snapshot = Some($crate::reexports::nameres::ModuleFsSnapshot::new( + &db, + std::collections::BTreeSet::new(), + std::collections::BTreeMap::new(), + )); + db.module_file_snapshot = + Some($crate::reexports::nameres::ModuleFileSnapshot::new( + &db, + std::collections::BTreeMap::new(), + )); + db + } + } + + #[salsa::db] + impl $crate::reexports::salsa::Database for $name {} + + #[salsa::db] + impl $crate::reexports::hir::Db for $name { + fn def_location_table<'db>( + &'db self, + file: $crate::reexports::hir::input::SourceFile, + ) -> &'db $crate::reexports::hir::anchor::DefLocationTable<'db> { + $crate::reexports::parser::parse_file_to_hir(self, file).def_locations(self) + } + } + + #[salsa::db] + impl $crate::reexports::parser::Db for $name {} + + #[salsa::db] + impl $crate::reexports::nameres::Db for $name { + fn module_tree(&self) -> $crate::reexports::nameres::ModuleTree { + self.module_tree + .expect("frontend test database module tree is initialized") + } + + fn module_fs_snapshot(&self) -> $crate::reexports::nameres::ModuleFsSnapshot { + self.module_fs_snapshot + .expect("frontend test database filesystem snapshot is initialized") + } + + fn module_file_snapshot(&self) -> $crate::reexports::nameres::ModuleFileSnapshot { + self.module_file_snapshot + .expect("frontend test database file snapshot is initialized") + } + + fn module_file<'db>( + &'db self, + module: $crate::reexports::nameres::ModuleId<'db>, + ) -> Option<$crate::reexports::hir::input::SourceFile> { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } + } + + #[salsa::db] + impl $typeck_crate::Db for $name {} + + impl $crate::FrontendTestDb for $name { + fn set_module_tree(&mut self, tree: $crate::reexports::nameres::ModuleTree) { + use $crate::reexports::salsa::Setter as _; + let main_root = tree.main_root(self).clone(); + let std_root = tree.std_root(self).clone(); + let external_roots = tree.external_roots(self).clone(); + let current = self + .module_tree + .expect("frontend test database module tree is initialized"); + if current.main_root(self) != &main_root { + current.set_main_root(self).to(main_root); + } + if current.std_root(self) != &std_root { + current.set_std_root(self).to(std_root); + } + if current.external_roots(self) != &external_roots { + current.set_external_roots(self).to(external_roots); + } + } + + fn set_module_fs_snapshot( + &mut self, + snapshot: $crate::reexports::nameres::ModuleFsSnapshot, + ) { + use $crate::reexports::salsa::Setter as _; + let existing_files = snapshot.existing_files(self).clone(); + let sibling_stems = snapshot.sibling_stems(self).clone(); + let current = self + .module_fs_snapshot + .expect("frontend test database filesystem snapshot is initialized"); + if current.existing_files(self) != &existing_files { + current.set_existing_files(self).to(existing_files); + } + if current.sibling_stems(self) != &sibling_stems { + current.set_sibling_stems(self).to(sibling_stems); + } + } + + fn insert_module_file( + &mut self, + key: $crate::reexports::nameres::ModuleKey, + file: $crate::reexports::hir::input::SourceFile, + ) { + use $crate::reexports::salsa::Setter as _; + let snapshot = self + .module_file_snapshot + .expect("frontend test database file snapshot is initialized"); + let mut files = snapshot.files(self).clone(); + if files.insert(key, file) == Some(file) { + return; + } + snapshot.set_files(self).to(files); + } + + fn contains_module_file(&self, key: &$crate::reexports::nameres::ModuleKey) -> bool { + self.module_file_snapshot + .expect("frontend test database file snapshot is initialized") + .files(self) + .contains_key(key) + } + + fn module_file_for_key( + &self, + key: &$crate::reexports::nameres::ModuleKey, + ) -> Option<$crate::reexports::hir::input::SourceFile> { + self.module_file_snapshot + .expect("frontend test database file snapshot is initialized") + .files(self) + .get(key) + .copied() + } + } + }; +} + +pub fn repo_root_from_manifest(manifest_dir: impl AsRef) -> PathBuf { + manifest_dir + .as_ref() + .parent() + .and_then(Path::parent) + .expect("crate lives under /crates/") + .to_path_buf() +} + +pub fn load_fixture_case( + db: &mut Db, + root: &Path, + repo_root: &Path, + external_roots: BTreeMap, +) -> ModuleKey +where + Db: FrontendTestDb, +{ + load_fixture_case_with_url_style(db, root, repo_root, external_roots, SourceUrlStyle::Memory) +} + +pub fn load_fixture_case_with_file_urls( + db: &mut Db, + root: &Path, + repo_root: &Path, + external_roots: BTreeMap, +) -> ModuleKey +where + Db: FrontendTestDb, +{ + load_fixture_case_with_url_style(db, root, repo_root, external_roots, SourceUrlStyle::File) +} + +fn load_fixture_case_with_url_style( + db: &mut Db, + root: &Path, + repo_root: &Path, + external_roots: BTreeMap, + url_style: SourceUrlStyle, +) -> ModuleKey +where + Db: FrontendTestDb, +{ + let std_root = repo_root.join("std"); + db.set_module_tree(ModuleTree::new( + db, + root.to_path_buf(), + std_root.clone(), + external_roots.clone(), + )); + db.set_module_fs_snapshot(module_fs_snapshot_for_roots( + db, + std::iter::once(root) + .chain(std::iter::once(std_root.as_path())) + .chain(external_roots.values().map(|path| path.as_path())), + )); + load_library_files(db, LibraryId::Main, root, root, url_style); + for (name, external_root) in external_roots { + load_library_files( + db, + LibraryId::External(name), + &external_root, + &external_root, + url_style, + ); + } + + let entry_path = root.join("main.solc"); + module_key_for_path(LibraryId::Main, root, &entry_path).expect("fixture main.solc key") +} + +pub fn load_main_source(db: &mut Db, source: &str) -> ModuleKey +where + Db: FrontendTestDb, +{ + db.set_module_tree(ModuleTree::new( + db, + PathBuf::from("/main"), + PathBuf::from("/std"), + BTreeMap::new(), + )); + db.set_module_fs_snapshot(ModuleFsSnapshot::new(db, BTreeSet::new(), BTreeMap::new())); + let key = ModuleKey { + library: LibraryId::Main, + logical_path: vec!["main".to_owned()], + }; + let file = SourceFile::new(db, fixture_url(&key), Some(source.to_owned())); + db.insert_module_file(key.clone(), file); + key +} + +pub fn load_reachable_modules(db: &mut Db, entry: ModuleKey) +where + Db: FrontendTestDb, +{ + load_reachable_modules_with_url_style(db, entry, SourceUrlStyle::Memory); +} + +pub fn load_reachable_modules_with_file_urls(db: &mut Db, entry: ModuleKey) +where + Db: FrontendTestDb, +{ + load_reachable_modules_with_url_style(db, entry, SourceUrlStyle::File); +} + +fn load_reachable_modules_with_url_style( + db: &mut Db, + entry: ModuleKey, + url_style: SourceUrlStyle, +) where + Db: FrontendTestDb, +{ + let mut queue = vec![entry]; + let mut visited = FxHashSet::default(); + + while let Some(key) = queue.pop() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = db.module_file_for_key(&key) else { + continue; + }; + let targets = { + let module = module_id_from_key(&*db, &key); + let refs = nameres::module_imports(&*db, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map(|path| { + let resolved = resolve_module_path_candidate(&*db, module, &path).ok()?; + Some((resolved.module.key(&*db), resolved.file_path)) + }) + .collect::>() + }; + + for (target_key, file_path) in targets { + if !db.contains_module_file(&target_key) && file_path.exists() { + let file = source_file_for_path(db, &target_key, &file_path, url_style); + db.insert_module_file(target_key.clone(), file); + } + if db.contains_module_file(&target_key) { + queue.push(target_key); + } + } + } +} + +pub fn parse_diagnostics_for_source(db: &Db, path: &str, source: &str) -> Vec +where + Db: hir::Db + parser::Db, +{ + let url = format!("memory:///main/{path}") + .parse() + .expect("fixture URL"); + let file = SourceFile::new(db, url, Some(source.to_owned())); + let _ = parser::parse_file_to_hir(db, file); + lower_any_diagnostics(db, parser::parse_diagnostics(db, file).iter().cloned()) +} + +pub fn nameres_diagnostics(db: &Db, entry: &ModuleKey) -> Vec +where + Db: FrontendTestDb, +{ + let entry = module_id_from_key(db, entry); + let _ = nameres::resolve_reachable_full(db, entry); + lower_any_diagnostics( + db, + nameres::reachable_diagnostics(db, entry).iter().cloned(), + ) +} + +pub fn lower_any_diagnostics( + db: &dyn hir::Db, + diagnostics: impl IntoIterator, +) -> Vec { + let mut diagnostics = diagnostics + .into_iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect::>(); + sort_dedup_diagnostics(db, &mut diagnostics); + diagnostics +} + +pub fn sort_dedup_diagnostics(db: &dyn hir::Db, diagnostics: &mut Vec) { + sort_dedup_rendered_diagnostics(db, diagnostics); +} + +pub fn render_diagnostics(db: &dyn hir::Db, diagnostics: &[Diagnostic]) -> String { + if diagnostics.is_empty() { + return "no diagnostics\n".to_owned(); + } + + let renderer = Renderer::plain(); + let mut output = String::new(); + for (idx, diagnostic) in diagnostics.iter().enumerate() { + if idx > 0 { + output.push_str("\n---\n\n"); + } + output.push_str(&diagnostic.render_with(db, &renderer)); + } + normalize_rendered(&output) +} + +pub fn assert_diagnostics_snapshot(fixture_root: &Path, rendered: &str) { + let mut settings = insta::Settings::new(); + settings.set_snapshot_path(fixture_root); + settings.set_input_file(fixture_root.join("main.solc")); + settings.set_prepend_module_to_snapshot(false); + settings.bind(|| { + insta::assert_snapshot!("diagnostics", rendered); + }); +} + +pub fn run_in_large_stack(assertion: impl FnOnce() + Send + 'static) { + let result = thread::Builder::new() + .stack_size(64 * 1024 * 1024) + .spawn(assertion) + .expect("spawn fixture assertion") + .join(); + if let Err(payload) = result { + panic::resume_unwind(payload); + } +} + +pub fn module_fs_snapshot_for_roots<'a, Db>( + db: &Db, + roots: impl IntoIterator, +) -> ModuleFsSnapshot +where + Db: FrontendTestDb, +{ + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for root in roots { + collect_module_fs_snapshot(root, &mut existing_files, &mut sibling_stems); + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + ModuleFsSnapshot::new(db, existing_files, sibling_stems) +} + +fn collect_module_fs_snapshot( + dir: &Path, + existing_files: &mut BTreeSet, + sibling_stems: &mut BTreeMap>, +) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) == Some("solc") { + if path.is_file() { + existing_files.insert(path.clone()); + } + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + sibling_stems + .entry(dir.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + if path.is_dir() { + collect_module_fs_snapshot(&path, existing_files, sibling_stems); + } + } +} + +fn load_library_files( + db: &mut Db, + library: LibraryId, + root: &Path, + dir: &Path, + url_style: SourceUrlStyle, +) where + Db: FrontendTestDb, +{ + for entry in fs::read_dir(dir).expect("read fixture directory") { + let path = entry.expect("fixture entry").path(); + if path.is_dir() { + load_library_files(db, library.clone(), root, &path, url_style); + } else if path.extension().and_then(|ext| ext.to_str()) == Some("solc") { + let key = module_key_for_path(library.clone(), root, &path).expect("module key"); + let file = source_file_for_path(db, &key, &path, url_style); + db.insert_module_file(key, file); + } + } +} + +#[derive(Clone, Copy)] +enum SourceUrlStyle { + Memory, + File, +} + +fn source_file_for_path( + db: &Db, + key: &ModuleKey, + path: &Path, + url_style: SourceUrlStyle, +) -> SourceFile +where + Db: hir::Db, +{ + let source = fs::read_to_string(path).expect("source file"); + let url = match url_style { + SourceUrlStyle::Memory => fixture_url(key), + SourceUrlStyle::File => Url::from_file_path(path).expect("fixture file URL"), + }; + SourceFile::new(db, url, Some(source)) +} + +fn fixture_url(key: &ModuleKey) -> Url { + let library = match &key.library { + LibraryId::Main => "main".to_owned(), + LibraryId::Std => "std".to_owned(), + LibraryId::External(name) => format!("external/{name}"), + }; + let path = key.logical_path.join("/"); + format!("memory:///{library}/{path}.solc") + .parse() + .expect("fixture memory URL") +} + +fn normalize_rendered(output: &str) -> String { + output.replace('\\', "/") +} diff --git a/crates/uitest/Cargo.toml b/crates/uitest/Cargo.toml new file mode 100644 index 00000000..a56f9c8b --- /dev/null +++ b/crates/uitest/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "solcore-uitest" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +hir = { workspace = true } +hir-ty = { workspace = true } +hull = { path = "../hull", package = "solcore-hull" } +nameres = { workspace = true } +parser = { workspace = true } +salsa = { workspace = true } +solcore-test-utils = { path = "../test-utils" } +specialize = { path = "../specialize", package = "solcore-specialize" } + +[dev-dependencies] +dir-test = "0.4.1" diff --git a/crates/uitest/src/lib.rs b/crates/uitest/src/lib.rs new file mode 100644 index 00000000..0e33a551 --- /dev/null +++ b/crates/uitest/src/lib.rs @@ -0,0 +1 @@ +//! Dev-only user-interface diagnostic tests. diff --git a/crates/uitest/tests/diagnostics.rs b/crates/uitest/tests/diagnostics.rs new file mode 100644 index 00000000..9a1bda5b --- /dev/null +++ b/crates/uitest/tests/diagnostics.rs @@ -0,0 +1,229 @@ +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use dir_test::{Fixture, dir_test}; +use hir::diag::Diagnostic; +use nameres::{Db as _, ModuleKey, module_id_from_key}; +use solcore_test_utils::{ + assert_diagnostics_snapshot, define_frontend_test_db, load_fixture_case, + load_fixture_case_with_file_urls, load_reachable_modules_with_file_urls, lower_any_diagnostics, + nameres_diagnostics, parse_diagnostics_for_source, render_diagnostics, repo_root_from_manifest, + run_in_large_stack, sort_dedup_diagnostics, +}; + +define_frontend_test_db!(TestDb, hir_ty); + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/parse", + glob: "**/main.solc" +)] +fn parse_fail_diagnostics(fixture: Fixture<&str>) { + let path = fixture.path().to_owned(); + let source = fixture.content().to_string(); + run_in_large_stack(move || { + let db = TestDb::default(); + let diagnostics = parse_diagnostics_for_source(&db, "main.solc", &source); + assert_failure_snapshot( + &db, + Path::new(&path).parent().expect("case dir"), + diagnostics, + ); + }); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/nameres", + glob: "**/main.solc" +)] +fn nameres_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case(fixture, |db, entry| nameres_diagnostics(db, &entry)); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/typeck", + glob: "**/main.solc" +)] +fn typeck_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case_with_dependencies(fixture, full_frontend_diagnostics); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/solver", + glob: "**/main.solc" +)] +fn solver_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case_with_dependencies(fixture, full_frontend_diagnostics); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/comptime", + glob: "**/main.solc" +)] +fn comptime_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case(fixture, specialize_diagnostics); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/specialize", + glob: "**/main.solc" +)] +fn specialize_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case(fixture, specialize_diagnostics); +} + +#[dir_test( + dir: "$CARGO_MANIFEST_DIR/tests/fixtures/hull", + glob: "**/main.solc" +)] +fn hull_fail_diagnostics(fixture: Fixture<&str>) { + run_fixture_case_with_dependencies(fixture, hull_diagnostics); +} + +fn run_fixture_case( + fixture: Fixture<&str>, + diagnostics: fn(&TestDb, ModuleKey) -> Vec, +) { + run_fixture_case_with_options(fixture, diagnostics, false); +} + +fn run_fixture_case_with_dependencies( + fixture: Fixture<&str>, + diagnostics: fn(&TestDb, ModuleKey) -> Vec, +) { + run_fixture_case_with_options(fixture, diagnostics, true); +} + +fn run_fixture_case_with_options( + fixture: Fixture<&str>, + diagnostics: fn(&TestDb, ModuleKey) -> Vec, + load_dependencies: bool, +) { + let case_dir = PathBuf::from(fixture.path()) + .parent() + .expect("case dir") + .to_path_buf(); + run_in_large_stack(move || { + let repo_root = repo_root_from_manifest(env!("CARGO_MANIFEST_DIR")); + let mut db = TestDb::default(); + let allows_success = allows_success_fixture(&case_dir); + let entry = if allows_success { + load_fixture_case_with_file_urls(&mut db, &case_dir, &repo_root, BTreeMap::new()) + } else { + load_fixture_case(&mut db, &case_dir, &repo_root, BTreeMap::new()) + }; + if allows_success || load_dependencies { + // Keep the entry's stable virtual URL for snapshots, but give + // imported dependencies their real file URLs so backend queries + // can recover canonical module identities from configured roots. + load_reachable_modules_with_file_urls(&mut db, entry.clone()); + let entry_id = module_id_from_key(&db, &entry); + let _ = nameres::resolve_reachable_full(&db, entry_id); + } + let diagnostics = diagnostics(&db, entry); + assert_failure_snapshot(&db, &case_dir, diagnostics); + }); +} + +fn full_frontend_diagnostics(db: &TestDb, entry: ModuleKey) -> Vec { + let entry = module_id_from_key(db, &entry); + let mut diagnostics = nameres::reachable_diagnostics(db, entry).to_vec(); + diagnostics.extend( + hir_ty::infer::reachable_typeck_diagnostics(db, entry) + .iter() + .cloned(), + ); + lower_any_diagnostics(db, diagnostics) +} + +fn specialize_diagnostics(db: &TestDb, entry: ModuleKey) -> Vec { + let entry = module_id_from_key(db, &entry); + let Some(file) = db.module_file(entry) else { + return Vec::new(); + }; + let module = parser::parse_file_to_hir(db, file).module(db); + let output = + specialize::specialize_module(db, module, specialize::SpecializeOptions::default()); + let mut diagnostics = output + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect::>(); + sort_dedup_diagnostics(db, &mut diagnostics); + diagnostics +} + +fn hull_diagnostics(db: &TestDb, entry: ModuleKey) -> Vec { + let entry = module_id_from_key(db, &entry); + let Some(file) = db.module_file(entry) else { + return Vec::new(); + }; + let module = parser::parse_file_to_hir(db, file).module(db); + // Hull failure fixtures deliberately exercise backend validation even for + // programs that the full frontend would reject first. Contract-surface + // errors are different: they make the generated std.dispatch wrapper + // invalid, so diagnose those source declarations before specialization. + let mut contract_diagnostics = hir_ty::module_contract_diagnostics(db, module); + sort_dedup_diagnostics(db, &mut contract_diagnostics); + if !contract_diagnostics.is_empty() { + return contract_diagnostics; + } + let output = + specialize::specialize_module(db, module, specialize::SpecializeOptions::default()); + let mut diagnostics = output + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)) + .collect::>(); + if !diagnostics.is_empty() { + sort_dedup_diagnostics(db, &mut diagnostics); + return diagnostics; + } + + let emitted = hull::emit_module(db, &output.module, hull::EmitOptions::default()); + diagnostics.extend( + emitted + .diagnostics + .iter() + .map(|diagnostic| diagnostic.lower(db)), + ); + if diagnostics.is_empty() { + diagnostics.extend( + hull::check_program_with_db(db, &emitted.program) + .iter() + .map(|diagnostic| diagnostic.lower(db)), + ); + } + sort_dedup_diagnostics(db, &mut diagnostics); + diagnostics +} + +fn assert_failure_snapshot(db: &TestDb, case_dir: &Path, diagnostics: Vec) { + if diagnostics.is_empty() { + assert!( + allows_success_fixture(case_dir), + "expected diagnostics for failure fixture `{}`", + case_dir.display() + ); + } + let rendered = render_diagnostics(db, &diagnostics); + assert_diagnostics_snapshot(case_dir, &rendered); +} + +fn allows_success_fixture(case_dir: &Path) -> bool { + let Some(parent) = case_dir + .parent() + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + else { + return false; + }; + if !matches!(parent, "typeck" | "specialize" | "hull") { + return false; + } + case_dir + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("ok_")) +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_asm_ret/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_asm_ret/diagnostics.snap new file mode 100644 index 00000000..f2cefdd3 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_asm_ret/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_asm_ret/main.solc +--- +error[SC0409]: comptime evaluation failed: function with a comptime return type returns a runtime expression + --> /main/main.solc:12:5 + | +11 | } +12 | return v; + | ^^^^^^^^^ comptime evaluation failed here +13 | } + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_asm_ret/main.solc b/crates/uitest/tests/fixtures/comptime/ct_asm_ret/main.solc new file mode 100644 index 00000000..83ffee12 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_asm_ret/main.solc @@ -0,0 +1,17 @@ +/* Negative: function annotated '-> comptime word' but body reads from + storage via sload — storage is mutable state, never comptime. + The verifier must reject this. +*/ + +contract ComptimeAsmRet { + function loadFromStorage() returns (comptime word) { + let v: word; + assembly { + v := sload(0) + } + return v; + } + function main() returns (word) { + return loadFromStorage(); + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_let_runtime/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_let_runtime/diagnostics.snap new file mode 100644 index 00000000..973ea21f --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_let_runtime/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_let_runtime/main.solc +--- +error[SC0409]: comptime evaluation failed: comptime let 'y' is bound to a runtime expression + --> /main/main.solc:18:5 + | +17 | function main() returns (word) { +18 | let comptime y: word = sloadWord(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +19 | return y; + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_let_runtime/main.solc b/crates/uitest/tests/fixtures/comptime/ct_let_runtime/main.solc new file mode 100644 index 00000000..526fcbeb --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_let_runtime/main.solc @@ -0,0 +1,21 @@ +/* Negative: comptime let bound to a runtime expression — must fail. + sloadWord reads from storage (sload); storage is mutable state, + so its result is runtime. Binding it with 'let y : comptime word' + must be rejected by the verifier. +*/ +import * as std from std; + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract ComptimeLetRuntime { + function main() returns (word) { + let comptime y: word = sloadWord(); + return y; + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/diagnostics.snap new file mode 100644 index 00000000..4579987d --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/diagnostics.snap @@ -0,0 +1,43 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/main.solc +--- +error[SC0409]: comptime evaluation failed: function with a comptime return type returns a runtime expression + --> /main/main.solc:18:5 + | +17 | } +18 | return base + x * factor; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +19 | } + | +--- + +error[SC0406]: missing evidence: add + --> /main/main.solc:18:12 + | +17 | } +18 | return base + x * factor; + | ^^^^^^^^^^^^^^^^^ trait evidence required here +19 | } + | +--- + +error[SC0406]: missing evidence: mul + --> /main/main.solc:18:19 + | +17 | } +18 | return base + x * factor; + | ^^^^^^^^^^ trait evidence required here +19 | } + | +--- + +error[SC0409]: comptime evaluation failed: comptime let 'a' is bound to a runtime expression + --> /main/main.solc:24:5 + | +23 | function main() returns (word) { +24 | let comptime a: word = Scale.scale(3, 10); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +25 | return a; + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/main.solc b/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/main.solc new file mode 100644 index 00000000..0bd6d392 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_overloaded_bad/main.solc @@ -0,0 +1,27 @@ +/* Negative: Scale instance whose 'scale' reads from storage — not comptime. + Despite the comptime annotations on the method signature, the word + instance body uses sload (mutable storage state), making the result + a runtime value. The verifier must reject the comptime let binding. +*/ +import * as std from std; + +trait Scale { + function scale(comptime factor: word, comptime x: a) returns (comptime a); +} + +impl Scale { + function scale(comptime factor: word, comptime x: word) returns (comptime word) { + let base: word; + assembly { + base := sload(0) + } + return base + x * factor; + } +} + +contract ComptimeOverloadedBad { + function main() returns (word) { + let comptime a: word = Scale.scale(3, 10); + return a; + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/diagnostics.snap new file mode 100644 index 00000000..88458977 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/main.solc +--- +error[SC0409]: comptime evaluation failed: runtime value passed to comptime parameter 'x' of 'unwrap' + --> /main/main.solc:20:10 + | +19 | function process(z: t) returns (word) where t: Wrap { +20 | return Wrap.unwrap(z); + | ^^^^^^^^^^^^^^ comptime evaluation failed here +21 | } + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/main.solc b/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/main.solc new file mode 100644 index 00000000..c24ddfc7 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_param_poly_runtime/main.solc @@ -0,0 +1,27 @@ +/* Negative: comptime violation in a polymorphic (generic) function. + Before specialisation the concrete type of 'z' is unknown, so this + cannot be resolved by inlining. The SAIL-level check catches the + violation: 'z' is a non-comptime parameter and cannot satisfy the + comptime contract of 'unwrap'. +*/ +import * as std from std; + +trait Wrap { + function unwrap(comptime x: t) returns (comptime word); +} + +impl Wrap { + function unwrap(comptime x: word) returns (comptime word) { + return x; + } +} + +function process(z: t) returns (word) where t: Wrap { + return Wrap.unwrap(z); +} + +contract ComptimeParamPolyRuntime { + function main() returns (word) { + return process(42); + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_param_runtime/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_param_runtime/diagnostics.snap new file mode 100644 index 00000000..ac075605 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_param_runtime/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_param_runtime/main.solc +--- +error[SC0406]: missing evidence: add + --> /main/main.solc:11:12 + | +10 | function double(comptime x: word) returns (comptime word) { +11 | return x + x; + | ^^^^^ trait evidence required here +12 | } + | +--- + +error[SC0409]: comptime evaluation failed: runtime value passed to comptime parameter 'x' of 'double' + --> /main/main.solc:14:12 + | +13 | function process(value: word) returns (word) { +14 | return double(value); + | ^^^^^^^^^^^^^ comptime evaluation failed here +15 | } + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_param_runtime/main.solc b/crates/uitest/tests/fixtures/comptime/ct_param_runtime/main.solc new file mode 100644 index 00000000..86887d98 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_param_runtime/main.solc @@ -0,0 +1,19 @@ +/* Negative: non-comptime function parameter passed to a comptime parameter. + Caught by the SAIL-level check: 'process' CAN be called with an argument + not known at compile time, which would violate the comptime requirement + of 'double'. The SAIL check rejects this on the parameter type alone, + before looking at specific call sites. +*/ +import * as std from std; + +contract ComptimeParamRuntime { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function process(value: word) returns (word) { + return double(value); + } + function main() returns (word) { + return process(21); + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/diagnostics.snap new file mode 100644 index 00000000..1db026b9 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ct_runtime_arg/main.solc +--- +error[SC0406]: missing evidence: add + --> /main/main.solc:17:12 + | +16 | function double(comptime x: word) returns (comptime word) { +17 | return x + x; + | ^^^^^ trait evidence required here +18 | } + | +--- + +error[SC0409]: comptime evaluation failed: runtime value passed to comptime parameter 'x' of 'double' + --> /main/main.solc:20:12 + | +19 | function main() returns (word) { +20 | return double(sloadWord()); + | ^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +21 | } + | diff --git a/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/main.solc b/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/main.solc new file mode 100644 index 00000000..c2e85a7e --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ct_runtime_arg/main.solc @@ -0,0 +1,22 @@ +/* Negative: runtime value passed to a comptime parameter — must fail. + sloadWord uses sload; storage is mutable state, so its result is + a runtime value; passing it to double's comptime param is an error. +*/ +import * as std from std; + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract ComptimeRuntimeArg { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + return double(sloadWord()); + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/diagnostics.snap new file mode 100644 index 00000000..0124fee6 --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/main.solc +--- +error[SC0410]: comptime evaluation fuel exhausted in spin at 128 unfold steps + --> /main/main.solc:6:10 + | +5 | function spin(comptime n: integer) returns (comptime integer) { +6 | return spin(integerAdd(n, 1)); + | ^^^^^^^^^^^^^^^^^^^^^^ comptime fuel limit reached here +7 | } + | + = note: comptime evaluation did not finish before the fuel limit was reached + = note: help: make the comptime recursion reach a base case, reduce the compile-time work, or raise --pe-fuel/--pe-depth when using solcore-driver diff --git a/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/main.solc b/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/main.solc new file mode 100644 index 00000000..bed8881f --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ergo_ct_fuel_infinite/main.solc @@ -0,0 +1,13 @@ +// Non-terminating comptime recursion: inline-depth exhaustion must stop the +// recursive evaluator before the larger total-work fuel budget is consumed. +import * as std from std; + +function spin(comptime n: integer) returns (comptime integer) { + return spin(integerAdd(n, 1)); +} + +contract CtFuelInfinite { + function main() returns (word) { + return wordFromInteger(spin(0)); + } +} diff --git a/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/diagnostics.snap b/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/diagnostics.snap new file mode 100644 index 00000000..2d09c99f --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/main.solc +--- +error[SC0401]: cannot specialize expression: type is not concrete + --> /main/main.solc:7:32 + | +6 | function scale(k: word) returns (word) { +7 | let comptime c: word = k + 1; + | ^ type must be concrete here +8 | return c; + | + = note: this can happen when a constructor or expression leaves a type parameter unresolved + = note: help: add a type annotation that fixes the concrete type diff --git a/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/main.solc b/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/main.solc new file mode 100644 index 00000000..e23979ac --- /dev/null +++ b/crates/uitest/tests/fixtures/comptime/ergo_ct_let_runtime_param/main.solc @@ -0,0 +1,17 @@ +// comptime let bound to a runtime function parameter: must fail comptime +// evaluation. The interesting question is span quality + cascade volume. +import * as std from std; + +contract CtLetRuntimeParam { + function scale(k: word) returns (word) { + let comptime c: word = k + 1; + return c; + } + function main() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return scale(v); + } +} diff --git a/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/diagnostics.snap b/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/diagnostics.snap new file mode 100644 index 00000000..c02a1784 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/assembly_assign_no_return/main.solc +--- +error[SC0445]: inline assembly assignment returns 0 values, expected 1 + --> /main/main.solc:6:12 + | +5 | assembly { +6 | x := mstore(1, 1) + | ^^^^^^^^^^^^ assembly return count mismatch +7 | } + | diff --git a/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/main.solc b/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/main.solc new file mode 100644 index 00000000..b1a4320b --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_assign_no_return/main.solc @@ -0,0 +1,10 @@ +// mstore does not return a value, so it cannot be assigned. +contract Test { + function main() public returns (word) { + let x: word; + assembly { + x := mstore(1, 1) + } + return x; + } +} diff --git a/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/diagnostics.snap b/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/diagnostics.snap new file mode 100644 index 00000000..3dafad76 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/assembly_assign_non_word/main.solc +--- +error[SC0434]: Hull type mismatch: expected (unit + unit), got word + --> /main/main.solc:7:9 + | +6 | function main() public returns (word) { +7 | let b: bool = false; + | ^ type mismatch +8 | assembly { b := add(1, 1) } + | +--- + +error[SC0448]: inline assembly assignment to `b` requires word type, got (unit + unit) + --> /main/main.solc:8:16 + | +7 | let b: bool = false; +8 | assembly { b := add(1, 1) } + | ^^^^^^^^^^^^^^ assembly assignment must be word +9 | if (b) { return 1; } else { return 0; } + | diff --git a/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/main.solc b/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/main.solc new file mode 100644 index 00000000..5442332f --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_assign_non_word/main.solc @@ -0,0 +1,11 @@ +// An assembly assignment writes a raw scalar word, so its LHS must have type +// 'word'. Assigning to a non-word local (here a 'bool', whose runtime layout +// is a tagged inl/inr pair) would corrupt that layout, so the type checker +// must reject this program. +contract AsmBool { + function main() public returns (word) { + let b: bool = false; + assembly { b := add(1, 1) } + if (b) { return 1; } else { return 0; } + } +} diff --git a/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/diagnostics.snap b/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/diagnostics.snap new file mode 100644 index 00000000..90148e27 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/main.solc +--- +error[SC0445]: inline assembly assignment returns 2 values, expected 3 + --> /main/main.solc:11:18 + | +10 | } +11 | x, y, z := pair() + | ^^^^^^ assembly return count mismatch +12 | } + | diff --git a/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/main.solc b/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/main.solc new file mode 100644 index 00000000..5e67a19d --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/assembly_multi_return_arity/main.solc @@ -0,0 +1,15 @@ +contract YulMultiRetBad { + function main() public returns (word) { + let x: word; + let y: word; + let z: word; + assembly { + function pair() -> a, b { + a := 1 + b := 2 + } + x, y, z := pair() + } + return x; + } +} diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/diagnostics.snap b/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/diagnostics.snap new file mode 100644 index 00000000..659096bb --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/main.solc +--- +error[SC0421]: cannot lower literal `"oops"` to Hull + --> /main/main.solc:5:12 + | +4 | function main() public returns (string) { +5 | return "oops"; + | ^^^^^^ unsupported literal +6 | } + | +--- + +error[SC0421]: cannot lower literal `"also bad"` to Hull + --> /main/main.solc:11:12 + | +10 | function main() public returns (string) { +11 | return "also bad"; + | ^^^^^^^^^^ unsupported literal +12 | } + | diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/main.solc b/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/main.solc new file mode 100644 index 00000000..e749ac19 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_multi_error/main.solc @@ -0,0 +1,13 @@ +// Two independent Hull-level problems in separate contracts: +// string literals are not representable in Hull. +contract First { + function main() public returns (string) { + return "oops"; + } +} + +contract Second { + function main() public returns (string) { + return "also bad"; + } +} diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/diagnostics.snap b/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/diagnostics.snap new file mode 100644 index 00000000..3b94e23b --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/ergo_hull_string_return/main.solc +--- +error[SC0421]: cannot lower literal `"42"` to Hull + --> /main/main.solc:5:12 + | +4 | function main() public { +5 | return "42"; + | ^^^^ unsupported literal +6 | } + | diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/main.solc b/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/main.solc new file mode 100644 index 00000000..aed702fc --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_string_return/main.solc @@ -0,0 +1,7 @@ +// Mirrors reference corpus test/examples/cases/string-const.solc: +// a public function returning a string constant. +contract Answer { + function main() public { + return "42"; + } +} diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/diagnostics.snap b/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/diagnostics.snap new file mode 100644 index 00000000..c27b224d --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:6:12 + | +5 | function name(d: uint256) public returns (uint256) { +6 | match (d) { + | ^ match is not exhaustive +7 | case 0 { return 100; } + | + = note: missing case: _ + = note: help: add a default or catch-all arm that covers the remaining values diff --git a/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/main.solc b/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/main.solc new file mode 100644 index 00000000..3d326d7e --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ergo_hull_word_match_no_default/main.solc @@ -0,0 +1,11 @@ +import std; +import std.dispatch; + +contract Digits { + function name(d: uint256) public returns (uint256) { + match (d) { + case 0 { return 100; } + case 1 { return 101; } + } + } +} diff --git a/crates/uitest/tests/fixtures/hull/non_exhaustive_match/diagnostics.snap b/crates/uitest/tests/fixtures/hull/non_exhaustive_match/diagnostics.snap new file mode 100644 index 00000000..740968a7 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/non_exhaustive_match/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/non_exhaustive_match/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:11:10 + | +10 | function onlyA(b: B) returns (word) { +11 | match (b) { + | ^ match is not exhaustive +12 | case B.A { return 1; } + | + = note: missing case: _ + = note: help: add a default or catch-all arm that covers the remaining values diff --git a/crates/uitest/tests/fixtures/hull/non_exhaustive_match/main.solc b/crates/uitest/tests/fixtures/hull/non_exhaustive_match/main.solc new file mode 100644 index 00000000..c440e5e8 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/non_exhaustive_match/main.solc @@ -0,0 +1,22 @@ +enum B { A, C }; + +function choose(x: bool) returns (B) { + if (x) { + return B.A; + } + return B.C; +} + +function onlyA(b: B) returns (word) { + match (b) { + case B.A { return 1; } + } +} + +contract C { + function main() public returns (word) { + let x: bool; + assembly { x := calldataload(0) } + return onlyA(choose(x)); + } +} diff --git a/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/diagnostics.snap b/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/diagnostics.snap new file mode 100644 index 00000000..2b82fff6 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/diagnostics.snap @@ -0,0 +1,6 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/main.solc +--- +no diagnostics diff --git a/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/main.solc b/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/main.solc new file mode 100644 index 00000000..738f89e8 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/ok_guarded_runtime_recursion/main.solc @@ -0,0 +1,15 @@ +import std; + +function countdown(n: word) returns (word) { + if (n == 0) { + return 0; + } else { + return countdown(n - 1); + } +} + +contract Counter { + function main() public returns (word) { + return countdown(3); + } +} diff --git a/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/diagnostics.snap b/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/diagnostics.snap new file mode 100644 index 00000000..3692880b --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/main.solc +--- +error[SC0430]: undefined Hull variable `content` + --> /main/main.solc:11:5 + | +10 | function set(value: bytes memory) public returns (()) { +11 | content = value; + | ^^^^^^^ undefined variable +12 | } + | +--- + +error[SC0430]: undefined Hull variable `content` + --> /main/main.solc:15:12 + | +14 | function get() public returns (bytes memory) { +15 | return content; + | ^^^^^^^ undefined variable +16 | } + | diff --git a/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/main.solc b/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/main.solc new file mode 100644 index 00000000..9b54fd08 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/unsupported_dispatch_storage/main.solc @@ -0,0 +1,17 @@ +import std; +import std.dispatch; + +// Storage support for a `memory(bytes)` contract field: assigning to the +// field copies the byte array into storage, reading it back loads it into +// fresh memory. Exercises StorageSize / CanStore for memory(bytes). +contract C { + content: bytes; + + function set(value: bytes memory) public returns (()) { + content = value; + } + + function get() public returns (bytes memory) { + return content; + } +} diff --git a/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/diagnostics.snap b/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/diagnostics.snap new file mode 100644 index 00000000..2a4db943 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/main.solc +--- +error[SC0231]: fallback ABI must have type `function()` + --> /main/main.solc:5:3 + | +4 | contract C { +5 | / fallback() returns (word) { +6 | | return 1; +7 | | } + | |___^ unsupported fallback ABI +8 | } + | diff --git a/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/main.solc b/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/main.solc new file mode 100644 index 00000000..230f07e1 --- /dev/null +++ b/crates/uitest/tests/fixtures/hull/unsupported_public_fallback_return/main.solc @@ -0,0 +1,8 @@ +import std; +import std.dispatch; + +contract C { + fallback() returns (word) { + return 1; + } +} diff --git a/crates/uitest/tests/fixtures/nameres/ambiguous/a.solc b/crates/uitest/tests/fixtures/nameres/ambiguous/a.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ambiguous/a.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/uitest/tests/fixtures/nameres/ambiguous/b.solc b/crates/uitest/tests/fixtures/nameres/ambiguous/b.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ambiguous/b.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/uitest/tests/fixtures/nameres/ambiguous/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ambiguous/diagnostics.snap new file mode 100644 index 00000000..8092b937 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ambiguous/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ambiguous/main.solc +--- +error[SC0120]: ambiguous selected import `value` in term namespace + --> /main/main.solc:1:1 + | +1 | import {value} from a; + | ^^^^^^^^^^^^^^^^^^^^^^ ambiguous selected import in term namespace +2 | import {value} from b; + | + = note: `value` is imported from a, b in term namespace + = note: use an explicit module qualifier or narrow the selected imports diff --git a/crates/uitest/tests/fixtures/nameres/ambiguous/main.solc b/crates/uitest/tests/fixtures/nameres/ambiguous/main.solc new file mode 100644 index 00000000..285a6509 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ambiguous/main.solc @@ -0,0 +1,2 @@ +import {value} from a; +import {value} from b; diff --git a/crates/uitest/tests/fixtures/nameres/clean_undefined_name/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/clean_undefined_name/diagnostics.snap new file mode 100644 index 00000000..7504dc22 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/clean_undefined_name/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/clean_undefined_name/main.solc +--- +error[SC0101]: undefined name: missing + --> /main/main.solc:1:43 + | +1 | function caller() returns (word) { return missing; } + | ^^^^^^^ unknown name diff --git a/crates/uitest/tests/fixtures/nameres/clean_undefined_name/main.solc b/crates/uitest/tests/fixtures/nameres/clean_undefined_name/main.solc new file mode 100644 index 00000000..21f048b4 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/clean_undefined_name/main.solc @@ -0,0 +1 @@ +function caller() returns (word) { return missing; } diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/a.solc b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/a.solc new file mode 100644 index 00000000..e2b3a2a4 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/a.solc @@ -0,0 +1,3 @@ +enum T { A }; + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/b.solc b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/b.solc new file mode 100644 index 00000000..89b0bcc5 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/b.solc @@ -0,0 +1,5 @@ +function T() returns (word) { + return 0; +} + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/diagnostics.snap new file mode 100644 index 00000000..c808d4fc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/main.solc +--- +error[SC0111]: duplicate exported item name `T` + --> /main/main.solc:2:11 + | +1 | export a.{T}; +2 | export b.{T}; + | ^ module exports this name more than once +3 | + | + = note: export each item name from only one origin diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/main.solc b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/main.solc new file mode 100644 index 00000000..765499d0 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_export_cross_namespace/main.solc @@ -0,0 +1,6 @@ +export a.{T}; +export b.{T}; + +function main() returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/diagnostics.snap new file mode 100644 index 00000000..a272b5de --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/diagnostics.snap @@ -0,0 +1,24 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/main.solc +--- +error[SC0108]: duplicate declaration `Foo` in type namespace + --> /main/main.solc:2:7 + | +1 | enum Foo { Foo }; + | --- previous declaration +2 | alias Foo = word; + | ^^^ duplicate declaration +3 | + | +--- + +error[SC0108]: duplicate declaration `dup` in term namespace + --> /main/main.solc:5:10 + | +3 | +4 | function dup() {} + | --- previous declaration +5 | function dup() {} + | ^^^ duplicate declaration diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/main.solc b/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/main.solc new file mode 100644 index 00000000..8fc62df2 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_local_declarations/main.solc @@ -0,0 +1,5 @@ +enum Foo { Foo }; +alias Foo = word; + +function dup() {} +function dup() {} diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/baz/bar.solc b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/baz/bar.solc new file mode 100644 index 00000000..60b02fe3 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/baz/bar.solc @@ -0,0 +1,3 @@ +function g() {} + +export { g }; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/diagnostics.snap new file mode 100644 index 00000000..0c534507 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/duplicate_qualifier/main.solc +--- +error[SC0116]: duplicate import qualifier `bar` + --> /main/main.solc:2:13 + | +1 | import * as bar from foo.bar; + | --- first qualifier with this name +2 | import * as bar from baz.bar; + | ^^^ duplicate import qualifier + | + = note: use an explicit alias to disambiguate one of the imports diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/foo/bar.solc b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/foo/bar.solc new file mode 100644 index 00000000..a7997dca --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/foo/bar.solc @@ -0,0 +1,3 @@ +function f() {} + +export { f }; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/main.solc b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/main.solc new file mode 100644 index 00000000..1154be80 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_qualifier/main.solc @@ -0,0 +1,2 @@ +import * as bar from foo.bar; +import * as bar from baz.bar; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_selector/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/duplicate_selector/diagnostics.snap new file mode 100644 index 00000000..81552950 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_selector/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/duplicate_selector/main.solc +--- +error[SC0117]: duplicate name `value` in selective import + --> /main/main.solc:1:16 + | +1 | import {value, value} from util; + | ----- ^^^^^ duplicate selected import + | | + | first selected import with this name + | + = note: list each selected or hidden name only once diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_selector/main.solc b/crates/uitest/tests/fixtures/nameres/duplicate_selector/main.solc new file mode 100644 index 00000000..c6939c45 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_selector/main.solc @@ -0,0 +1 @@ +import {value, value} from util; diff --git a/crates/uitest/tests/fixtures/nameres/duplicate_selector/util.solc b/crates/uitest/tests/fixtures/nameres/duplicate_selector/util.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/duplicate_selector/util.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/diagnostics.snap new file mode 100644 index 00000000..2fb50615 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/diagnostics.snap @@ -0,0 +1,30 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/main.solc +--- +error[SC0108]: duplicate declaration `Shape` in type namespace + --> /main/main.solc:3:6 + | +1 | enum Shape { Circle(word) }; + | ----- previous declaration +2 | +3 | enum Shape { Square(word) }; + | ^^^^^ duplicate declaration +4 | + | +--- + +error[SC0108]: duplicate declaration `Render` in type namespace + --> /main/main.solc:9:7 + | + 4 | + 5 | trait Render { + | ------ previous declaration + 6 | function render(x: a) returns (word); + 7 | } + 8 | + 9 | trait Render { + | ^^^^^^ duplicate declaration +10 | function paint(x: a) returns (word); + | diff --git a/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/main.solc new file mode 100644 index 00000000..e19e6846 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_dup_data_class/main.solc @@ -0,0 +1,11 @@ +enum Shape { Circle(word) }; + +enum Shape { Square(word) }; + +trait Render { + function render(x: a) returns (word); +} + +trait Render { + function paint(x: a) returns (word); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_dup_function/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_dup_function/diagnostics.snap new file mode 100644 index 00000000..4239cc88 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_dup_function/diagnostics.snap @@ -0,0 +1,18 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_dup_function/main.solc +--- +error[SC0108]: duplicate declaration `twice` in term namespace + --> /main/main.solc:9:10 + | + 1 | function twice(x: word) returns (word) { + | ----- previous declaration + 2 | return x; + 3 | } +... + 8 | + 9 | function twice(x: word) returns (word) { + | ^^^^^ duplicate declaration +10 | return x; + | diff --git a/crates/uitest/tests/fixtures/nameres/ergo_dup_function/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_dup_function/main.solc new file mode 100644 index 00000000..e46ad4a7 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_dup_function/main.solc @@ -0,0 +1,11 @@ +function twice(x: word) returns (word) { + return x; +} + +function helper(y: word) returns (word) { + return y; +} + +function twice(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/diagnostics.snap new file mode 100644 index 00000000..2e2e8dae --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/main.solc +--- +error[SC0109]: import helprs: file not found + --> /main/main.solc:1:27 + | +1 | import {helperValue} from helprs; + | ^^^^^^ module reference +2 | +3 | function main() returns (word) { + | + = help: check the module path or add the missing source file + = help: did you mean `helpers`? diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/helpers.solc b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/helpers.solc new file mode 100644 index 00000000..057993ab --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/helpers.solc @@ -0,0 +1,5 @@ +export { helperValue }; + +function helperValue(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/main.solc new file mode 100644 index 00000000..dca48485 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_module_typo/main.solc @@ -0,0 +1,5 @@ +import {helperValue} from helprs; + +function main() returns (word) { + return helperValue(1); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/diagnostics.snap new file mode 100644 index 00000000..1f07d229 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/main.solc +--- +error[SC0110]: unknown import item `valu` + --> /main/main.solc:1:9 + | +1 | import {valu} from util; + | ^^^^ unknown import item +2 | +3 | function main() returns (word) { + | + = note: `valu` is not exported by module `util` + = help: did you mean `value`? + = help: check the imported module's exported names diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/main.solc new file mode 100644 index 00000000..f57c8e4e --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/main.solc @@ -0,0 +1,5 @@ +import {valu} from util; + +function main() returns (word) { + return valu(1); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/util.solc b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/util.solc new file mode 100644 index 00000000..9e614c51 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_import_symbol_typo/util.solc @@ -0,0 +1,5 @@ +export { value }; + +function value(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/diagnostics.snap new file mode 100644 index 00000000..b1ece5b2 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_private_qualified/main.solc +--- +error[SC0101]: undefined name: secret + --> /main/main.solc:4:16 + | +3 | function main() returns (word) { +4 | return vault.secret(1); + | ^^^^^^ unknown name +5 | } + | + ::: /main/vault.solc:6 + | +6 | +7 | function secret(x: word) returns (word) { + | ------ private item declared here +8 | return x; + | + = note: `secret` is private to module `vault` and is not exported diff --git a/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/main.solc new file mode 100644 index 00000000..0eef09c4 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/main.solc @@ -0,0 +1,5 @@ +import * as vault from vault; + +function main() returns (word) { + return vault.secret(1); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/vault.solc b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/vault.solc new file mode 100644 index 00000000..36917988 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_private_qualified/vault.solc @@ -0,0 +1,9 @@ +export { opened }; + +function opened(x: word) returns (word) { + return secret(x); +} + +function secret(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/diagnostics.snap new file mode 100644 index 00000000..4261bbfa --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/main.solc +--- +error[SC0101]: undefined name: computeVale + --> /main/main.solc:6:10 + | +5 | function main() returns (word) { +6 | return computeVale(1); + | ^^^^^^^^^^^ unknown name +7 | } + | + = help: did you mean `computeValue`? diff --git a/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/main.solc new file mode 100644 index 00000000..b3c9eccc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_typo_did_you_mean/main.solc @@ -0,0 +1,7 @@ +function computeValue(x: word) returns (word) { + return x; +} + +function main() returns (word) { + return computeVale(1); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_class/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_undef_class/diagnostics.snap new file mode 100644 index 00000000..d97a49f5 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_class/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_undef_class/main.solc +--- +error[SC0105]: undefined trait: NoSuchClass + --> /main/main.solc:1:6 + | +1 | impl NoSuchClass { + | ^^^^^^^^^^^ undefined trait +2 | function frob(x: word) returns (word) { +3 | return x; + | diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_class/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_undef_class/main.solc new file mode 100644 index 00000000..78d226c5 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_class/main.solc @@ -0,0 +1,5 @@ +impl NoSuchClass { + function frob(x: word) returns (word) { + return x; + } +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/diagnostics.snap new file mode 100644 index 00000000..64bb58a3 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/main.solc +--- +error[SC0101]: undefined name: Option.Nope + --> /main/main.solc:5:15 + | +4 | match (o) { +5 | case Option.Nope { return 0; } + | ^^^^ unknown name +6 | case Option.Some(v) { return v; } + | + = help: did you mean `Option.None`? diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/main.solc new file mode 100644 index 00000000..9b986ee3 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_constructor/main.solc @@ -0,0 +1,8 @@ +enum Option { None, Some(word) }; + +function unwrap(o: Option) returns (word) { + match (o) { + case Option.Nope { return 0; } + case Option.Some(v) { return v; } + } +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_type/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_undef_type/diagnostics.snap new file mode 100644 index 00000000..92099873 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_type/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_undef_type/main.solc +--- +error[SC0103]: undefined type constructor: MissingType + --> /main/main.solc:1:20 + | +1 | function takeIt(x: MissingType) returns (word) { + | ^^^^^^^^^^^ undefined type constructor +2 | return 0; +3 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_type/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_undef_type/main.solc new file mode 100644 index 00000000..e73ef31b --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_type/main.solc @@ -0,0 +1,3 @@ +function takeIt(x: MissingType) returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/diagnostics.snap new file mode 100644 index 00000000..d023ebd5 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_undef_variable/main.solc +--- +error[SC0101]: undefined name: missingVar + --> /main/main.solc:2:14 + | +1 | function addOne(x: word) returns (word) { +2 | return x + missingVar; + | ^^^^^^^^^^ unknown name +3 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/main.solc new file mode 100644 index 00000000..996c4b93 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_undef_variable/main.solc @@ -0,0 +1,3 @@ +function addOne(x: word) returns (word) { + return x + missingVar; +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/diagnostics.snap new file mode 100644 index 00000000..843e8335 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/main.solc +--- +error[SC0106]: unqualified constructor: On + --> /main/main.solc:13:15 + | +12 | function main() returns (word) { +13 | return isOn(On); + | ^^ constructor must be qualified +14 | } + | + = help: use `Light.On` diff --git a/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/main.solc new file mode 100644 index 00000000..8cc998b2 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_unqual_ctor_sc0106/main.solc @@ -0,0 +1,14 @@ +// migrate-syntax: keep-unqualified-constructor +enum Light { On, Off }; +enum Power { Plugged, Battery }; + +function isOn(l: Light) returns (word) { + match (l) { + case Light.On { return 1; } + case Light.Off { return 0; } + } +} + +function main() returns (word) { + return isOn(On); +} diff --git a/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/diagnostics.snap new file mode 100644 index 00000000..ddef3038 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/ergo_value_as_type/main.solc +--- +error[SC0103]: undefined type constructor: MkPair + --> /main/main.solc:3:19 + | +1 | enum Pair { MkPair(word, word) }; + | ------ constructor declared here +2 | +3 | function first(p: MkPair) returns (word) { + | ^^^^^^ undefined type constructor +4 | match (p) { + | + = note: `MkPair` is a constructor of type `Pair` + = help: use `Pair` as the type name diff --git a/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/main.solc b/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/main.solc new file mode 100644 index 00000000..19d4ba41 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/ergo_value_as_type/main.solc @@ -0,0 +1,7 @@ +enum Pair { MkPair(word, word) }; + +function first(p: MkPair) returns (word) { + match (p) { + case Pair.MkPair(a, b) { return a; } + } +} diff --git a/crates/uitest/tests/fixtures/nameres/glob_shadow_local/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/diagnostics.snap new file mode 100644 index 00000000..6ac28d85 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/glob_shadow_local/main.solc +--- +error[SC0108]: duplicate declaration `value` in term namespace + --> /main/main.solc:3:10 + | +1 | import lib; + | ----------- previous declaration +2 | +3 | function value(x: word) returns (word) { + | ^^^^^ duplicate declaration +4 | return x; + | diff --git a/crates/uitest/tests/fixtures/nameres/glob_shadow_local/lib.solc b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/lib.solc new file mode 100644 index 00000000..36dd500c --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/lib.solc @@ -0,0 +1,5 @@ +function value(x: word) returns (word) { + return x; +} + +export { value }; diff --git a/crates/uitest/tests/fixtures/nameres/glob_shadow_local/main.solc b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/main.solc new file mode 100644 index 00000000..04c97a5a --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/glob_shadow_local/main.solc @@ -0,0 +1,5 @@ +import lib; + +function value(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/nameres/hidden_ctor/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/hidden_ctor/diagnostics.snap new file mode 100644 index 00000000..5d3f052a --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/hidden_ctor/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/hidden_ctor/main.solc +--- +error[SC0101]: undefined name: Err + --> /main/main.solc:4:16 + | +3 | function main() returns (Token) { +4 | return Token.Err(0); + | ^^^ unknown name +5 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/hidden_ctor/lib.solc b/crates/uitest/tests/fixtures/nameres/hidden_ctor/lib.solc new file mode 100644 index 00000000..7b03913c --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/hidden_ctor/lib.solc @@ -0,0 +1,7 @@ +export { Token(Ok), mkErr }; + +enum Token { Ok(word), Err(word) }; + +function mkErr(x: word) returns (Token) { + return Token.Err(x); +} diff --git a/crates/uitest/tests/fixtures/nameres/hidden_ctor/main.solc b/crates/uitest/tests/fixtures/nameres/hidden_ctor/main.solc new file mode 100644 index 00000000..fb35bb00 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/hidden_ctor/main.solc @@ -0,0 +1,5 @@ +import {Token} from lib; + +function main() returns (Token) { + return Token.Err(0); +} diff --git a/crates/uitest/tests/fixtures/nameres/missing/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/missing/diagnostics.snap new file mode 100644 index 00000000..ac42a0f7 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/missing/diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/missing/main.solc +--- +error[SC0109]: import missing: file not found + --> /main/main.solc:1:21 + | +1 | import {value} from missing; + | ^^^^^^^ module reference + | + = help: check the module path or add the missing source file diff --git a/crates/uitest/tests/fixtures/nameres/missing/main.solc b/crates/uitest/tests/fixtures/nameres/missing/main.solc new file mode 100644 index 00000000..7babefc7 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/missing/main.solc @@ -0,0 +1 @@ +import {value} from missing; diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/a.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/a.solc new file mode 100644 index 00000000..e2b3a2a4 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/a.solc @@ -0,0 +1,3 @@ +enum T { A }; + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/b.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/b.solc new file mode 100644 index 00000000..89b0bcc5 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/b.solc @@ -0,0 +1,5 @@ +function T() returns (word) { + return 0; +} + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/diagnostics.snap new file mode 100644 index 00000000..a8e320dc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/main.solc +--- +error[SC0120]: ambiguous selected import `T` across term/type namespaces + --> /main/main.solc:1:1 + | +1 | import {T} from a; + | ^^^^^^^^^^^^^^^^^^ ambiguous selected import across term/type namespaces +2 | import {T} from b; +3 | + | + = note: `T` is imported from a, b across term/type namespaces + = note: use an explicit module qualifier or narrow the selected imports diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/main.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/main.solc new file mode 100644 index 00000000..32ab0288 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_cross_namespace/main.solc @@ -0,0 +1,6 @@ +import {T} from a; +import {T} from b; + +function main() returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/a.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/a.solc new file mode 100644 index 00000000..1f9ea2fc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/a.solc @@ -0,0 +1,7 @@ +enum T { A }; + +function T() returns (word) { + return 0; +} + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/b.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/b.solc new file mode 100644 index 00000000..1f9ea2fc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/b.solc @@ -0,0 +1,7 @@ +enum T { A }; + +function T() returns (word) { + return 0; +} + +export { T }; diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/diagnostics.snap new file mode 100644 index 00000000..350ea4d6 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/diagnostics.snap @@ -0,0 +1,27 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/main.solc +--- +error[SC0120]: ambiguous selected import `T` in term namespace + --> /main/main.solc:1:1 + | +1 | import {T} from a; + | ^^^^^^^^^^^^^^^^^^ ambiguous selected import in term namespace +2 | import {T} from b; +3 | + | + = note: `T` is imported from a, b in term namespace + = note: use an explicit module qualifier or narrow the selected imports +--- + +error[SC0120]: ambiguous selected import `T` in type namespace + --> /main/main.solc:1:1 + | +1 | import {T} from a; + | ^^^^^^^^^^^^^^^^^^ ambiguous selected import in type namespace +2 | import {T} from b; +3 | + | + = note: `T` is imported from a, b in type namespace + = note: use an explicit module qualifier or narrow the selected imports diff --git a/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/main.solc b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/main.solc new file mode 100644 index 00000000..32ab0288 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/selected_import_ambiguity_namespace_identity/main.solc @@ -0,0 +1,6 @@ +import {T} from a; +import {T} from b; + +function main() returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/nameres/string_type_annotation/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/string_type_annotation/diagnostics.snap new file mode 100644 index 00000000..c2483622 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/string_type_annotation/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/string_type_annotation/main.solc +--- +error[SC0103]: undefined type constructor: string + --> /main/main.solc:1:23 + | +1 | function f() returns (string) { + | ^^^^^^ undefined type constructor +2 | return "ok"; +3 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/string_type_annotation/main.solc b/crates/uitest/tests/fixtures/nameres/string_type_annotation/main.solc new file mode 100644 index 00000000..8c0cfdbf --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/string_type_annotation/main.solc @@ -0,0 +1,3 @@ +function f() returns (string) { + return "ok"; +} diff --git a/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/diagnostics.snap new file mode 100644 index 00000000..29a7bdef --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/diagnostics.snap @@ -0,0 +1,33 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/main.solc +--- +error[SC0103]: undefined type constructor: MissingTy + --> /main/main.solc:1:18 + | +1 | function f(x: MissingTy) returns (word) where a: MissingClass { + | ^^^^^^^^^ undefined type constructor +2 | return missingName; +3 | } + | +--- + +error[SC0105]: undefined trait: MissingClass + --> /main/main.solc:1:53 + | +1 | function f(x: MissingTy) returns (word) where a: MissingClass { + | ^^^^^^^^^^^^ undefined trait +2 | return missingName; +3 | } + | +--- + +error[SC0101]: undefined name: missingName + --> /main/main.solc:2:10 + | +1 | function f(x: MissingTy) returns (word) where a: MissingClass { +2 | return missingName; + | ^^^^^^^^^^^ unknown name +3 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/main.solc b/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/main.solc new file mode 100644 index 00000000..f2b3c1fc --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/undefined_name_namespaces/main.solc @@ -0,0 +1,3 @@ +function f(x: MissingTy) returns (word) where a: MissingClass { + return missingName; +} diff --git a/crates/uitest/tests/fixtures/nameres/unknown_import/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unknown_import/diagnostics.snap new file mode 100644 index 00000000..eb78d526 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unknown_import/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unknown_import/main.solc +--- +error[SC0110]: unknown import item `missing` + --> /main/main.solc:1:9 + | +1 | import {missing} from util; + | ^^^^^^^ unknown import item + | + = note: `missing` is not exported by module `util` + = help: check the imported module's exported names diff --git a/crates/uitest/tests/fixtures/nameres/unknown_import/main.solc b/crates/uitest/tests/fixtures/nameres/unknown_import/main.solc new file mode 100644 index 00000000..e08ec7b1 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unknown_import/main.solc @@ -0,0 +1 @@ +import {missing} from util; diff --git a/crates/uitest/tests/fixtures/nameres/unknown_import/util.solc b/crates/uitest/tests/fixtures/nameres/unknown_import/util.solc new file mode 100644 index 00000000..c42ddf10 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unknown_import/util.solc @@ -0,0 +1,3 @@ +function value() {} + +export { value }; diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/diagnostics.snap new file mode 100644 index 00000000..74bfac2d --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/diagnostics.snap @@ -0,0 +1,58 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/main.solc +--- +error[SC0106]: unqualified constructor: Some + --> /main/main.solc:5:54 + | +4 | +5 | function exprCall(x: word) returns (Option) { return Some(x); } + | ^^^^ constructor must be qualified +6 | function exprBare(f: flag) returns (flag) { return on; } + | + = help: use `Option.Some` +--- + +error[SC0106]: unqualified constructor: on + --> /main/main.solc:6:52 + | +5 | function exprCall(x: word) returns (Option) { return Some(x); } +6 | function exprBare(f: flag) returns (flag) { return on; } + | ^^ constructor must be qualified +7 | + | + = help: use `flag.on` +--- + +error[SC0106]: unqualified constructor: off + --> /main/main.solc:10:8 + | + 9 | match (f) { +10 | case off { return 0; } + | ^^^ constructor must be qualified +11 | case on { return 1; } + | + = help: use `flag.off` +--- + +error[SC0106]: unqualified constructor: on + --> /main/main.solc:11:8 + | +10 | case off { return 0; } +11 | case on { return 1; } + | ^^ constructor must be qualified +12 | } + | + = help: use `flag.on` +--- + +error[SC0106]: unqualified constructor: None + --> /main/main.solc:17:8 + | +16 | match (o) { +17 | case None { return 0; } + | ^^^^ constructor must be qualified +18 | default { return 1; } + | + = help: use `Option.None` diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/main.solc new file mode 100644 index 00000000..26b57fa4 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_constructor_all_forms/main.solc @@ -0,0 +1,20 @@ +// migrate-syntax: keep-unqualified-constructor +enum Option { None, Some(word) }; +enum flag { off, on }; + +function exprCall(x: word) returns (Option) { return Some(x); } +function exprBare(f: flag) returns (flag) { return on; } + +function patLower(f: flag) returns (word) { + match (f) { + case off { return 0; } + case on { return 1; } + } +} + +function patUpper(o: Option) returns (word) { + match (o) { + case None { return 0; } + default { return 1; } + } +} diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/diagnostics.snap new file mode 100644 index 00000000..2749baea --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/main.solc +--- +error[SC0106]: unqualified constructor: on + --> /main/main.solc:12:15 + | +11 | function main() returns (word) { +12 | return pick(on); + | ^^ constructor must be qualified +13 | } + | + = help: use `flag.on` diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/main.solc new file mode 100644 index 00000000..d93f08aa --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_expr/main.solc @@ -0,0 +1,13 @@ +// migrate-syntax: keep-unqualified-constructor +enum flag { off, on }; + +function pick(f: flag) returns (word) { + match (f) { + case flag.off { return 0; } + case flag.on { return 1; } + } +} + +function main() returns (word) { + return pick(on); +} diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/diagnostics.snap new file mode 100644 index 00000000..65b02d18 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/main.solc +--- +error[SC0106]: unqualified constructor: Ok + --> /main/main.solc:5:10 + | +4 | function mk(x: word) returns (Token) { +5 | return Ok(x); + | ^^ constructor must be qualified +6 | } + | + = help: use Type.Constructor form +--- + +error[SC0106]: unqualified constructor: Ok + --> /main/main.solc:10:8 + | + 9 | match (t) { +10 | case Ok(v) { return v; } + | ^^ constructor must be qualified +11 | case Token.Err(v) { return v; } + | + = help: use Type.Constructor form diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/lib.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/lib.solc new file mode 100644 index 00000000..b9334fbb --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/lib.solc @@ -0,0 +1,3 @@ +export { Token(Ok, Err) }; + +enum Token { Ok(word), Err(word) }; diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/main.solc new file mode 100644 index 00000000..be89273c --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_imported/main.solc @@ -0,0 +1,13 @@ +// migrate-syntax: keep-unqualified-constructor +import {Token} from lib; + +function mk(x: word) returns (Token) { + return Ok(x); +} + +function classify(t: Token) returns (word) { + match (t) { + case Ok(v) { return v; } + case Token.Err(v) { return v; } + } +} diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/diagnostics.snap new file mode 100644 index 00000000..15861f7b --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/main.solc +--- +error[SC0106]: unqualified constructor: off + --> /main/main.solc:6:8 + | +5 | match (f) { +6 | case off { return 0; } + | ^^^ constructor must be qualified +7 | case on { return 1; } + | + = help: use `flag.off` +--- + +error[SC0106]: unqualified constructor: on + --> /main/main.solc:7:8 + | +6 | case off { return 0; } +7 | case on { return 1; } + | ^^ constructor must be qualified +8 | } + | + = help: use `flag.on` diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/main.solc new file mode 100644 index 00000000..b5b413eb --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern/main.solc @@ -0,0 +1,13 @@ +// migrate-syntax: keep-unqualified-constructor +enum flag { off, on }; + +function pick(f: flag) returns (word) { + match (f) { + case off { return 0; } + case on { return 1; } + } +} + +function main() returns (word) { + return pick(flag.on); +} diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/diagnostics.snap new file mode 100644 index 00000000..81437789 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/main.solc +--- +error[SC0106]: unqualified constructor: north + --> /main/main.solc:6:8 + | +5 | match (d) { +6 | case north { return 1; } + | ^^^^^ constructor must be qualified +7 | case south { return 2; } + | + = help: use `direction.north` +--- + +error[SC0106]: unqualified constructor: south + --> /main/main.solc:7:8 + | +6 | case north { return 1; } +7 | case south { return 2; } + | ^^^^^ constructor must be qualified +8 | } + | + = help: use `direction.south` diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/main.solc new file mode 100644 index 00000000..f609f06f --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_pattern_direction/main.solc @@ -0,0 +1,13 @@ +// migrate-syntax: keep-unqualified-constructor +enum direction { north, south }; + +function pick(d: direction) returns (word) { + match (d) { + case north { return 1; } + case south { return 2; } + } +} + +function main() returns (word) { + return pick(direction.south); +} diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/diagnostics.snap new file mode 100644 index 00000000..c8053039 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/main.solc +--- +error[SC0106]: unqualified constructor: wrapper + --> /main/main.solc:6:8 + | +5 | match (u) { +6 | case wrapper(w) { return w; } + | ^^^^^^^ constructor must be qualified +7 | } + | + = help: use Type.Constructor form +--- + +error[SC0106]: unqualified constructor: wrapper + --> /main/main.solc:11:17 + | +10 | function main() returns (word) { +11 | return unwrap(wrapper(3)); + | ^^^^^^^ constructor must be qualified +12 | } + | + = help: use Type.Constructor form diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/lib.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/lib.solc new file mode 100644 index 00000000..3669e3d0 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/lib.solc @@ -0,0 +1,3 @@ +export { wrapper(wrapper) }; + +enum wrapper { wrapper(word) }; diff --git a/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/main.solc b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/main.solc new file mode 100644 index 00000000..0cdd4fc3 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unqualified_ctor_plain_import/main.solc @@ -0,0 +1,12 @@ +// migrate-syntax: keep-unqualified-constructor +import * as lib from lib; + +function unwrap(u: lib.wrapper) returns (word) { + match (u) { + case wrapper(w) { return w; } + } +} + +function main() returns (word) { + return unwrap(wrapper(3)); +} diff --git a/crates/uitest/tests/fixtures/nameres/unresolved_qualified/diagnostics.snap b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/diagnostics.snap new file mode 100644 index 00000000..c3cc371e --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/nameres/unresolved_qualified/main.solc +--- +error[SC0101]: undefined name: missing + --> /main/main.solc:4:15 + | +3 | function main() returns (word) { +4 | return util.missing(); + | ^^^^^^^ unknown name +5 | } + | diff --git a/crates/uitest/tests/fixtures/nameres/unresolved_qualified/main.solc b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/main.solc new file mode 100644 index 00000000..64a25705 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/main.solc @@ -0,0 +1,5 @@ +import * as util from util; + +function main() returns (word) { + return util.missing(); +} diff --git a/crates/uitest/tests/fixtures/nameres/unresolved_qualified/util.solc b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/util.solc new file mode 100644 index 00000000..41eb3ad7 --- /dev/null +++ b/crates/uitest/tests/fixtures/nameres/unresolved_qualified/util.solc @@ -0,0 +1,5 @@ +export { value }; + +function value() returns (word) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/diagnostics.snap b/crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/diagnostics.snap new file mode 100644 index 00000000..7598c808 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/main.solc +--- +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:4:4 + | +3 | mstore(0, 0) +4 | }; + | ^ unexpected token +5 | } + | + = note: expecting end of input, or statement diff --git a/crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.solc b/crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/assembly_trailing_semicolon.solc rename to crates/uitest/tests/fixtures/parse/assembly_trailing_semicolon/main.solc diff --git a/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/diagnostics.snap b/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/diagnostics.snap new file mode 100644 index 00000000..bc964c1a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:3:1 + | +1 | function bad() { +2 | x = 1 +3 | } + | ^ unexpected token + | + = note: expecting `&&`, `&`, `(`, `*`, `.`, `;`, `<`, `>`, `?`, `[`, `^`, `as`, `|`, or `||` diff --git a/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/main.solc b/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/main.solc new file mode 100644 index 00000000..1b2f6dc8 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/assignment_missing_semicolon/main.solc @@ -0,0 +1,3 @@ +function bad() { + x = 1 +} diff --git a/crates/uitest/tests/fixtures/parse/body_independent_errors/diagnostics.snap b/crates/uitest/tests/fixtures/parse/body_independent_errors/diagnostics.snap new file mode 100644 index 00000000..52de7ba7 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/body_independent_errors/diagnostics.snap @@ -0,0 +1,24 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/body_independent_errors/main.solc +--- +error[SC0001]: invalid token `§` + --> /main/main.solc:2:1 + | +1 | function main() returns (word) { +2 | § + | ^ invalid token +3 | let broken = ; + | +--- + +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:3:14 + | +2 | § +3 | let broken = ; + | ^ unexpected token +4 | return 0; + | + = note: expecting expression after `=` diff --git a/crates/uitest/tests/fixtures/parse/body_independent_errors/main.solc b/crates/uitest/tests/fixtures/parse/body_independent_errors/main.solc new file mode 100644 index 00000000..38461834 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/body_independent_errors/main.solc @@ -0,0 +1,5 @@ +function main() returns (word) { +§ +let broken = ; +return 0; +} diff --git a/crates/uitest/tests/fixtures/parse/body_invalid_token/diagnostics.snap b/crates/uitest/tests/fixtures/parse/body_invalid_token/diagnostics.snap new file mode 100644 index 00000000..ed992719 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/body_invalid_token/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/body_invalid_token/main.solc +--- +error[SC0001]: invalid token `§` + --> /main/main.solc:1:41 + | +1 | function main() returns (word) { return §; } + | ^ invalid token diff --git a/crates/uitest/tests/fixtures/parse/body_invalid_token/main.solc b/crates/uitest/tests/fixtures/parse/body_invalid_token/main.solc new file mode 100644 index 00000000..a18154fc --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/body_invalid_token/main.solc @@ -0,0 +1 @@ +function main() returns (word) { return §; } diff --git a/crates/uitest/tests/fixtures/parse/bom_only_file/diagnostics.snap b/crates/uitest/tests/fixtures/parse/bom_only_file/diagnostics.snap new file mode 100644 index 00000000..d715561c --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/bom_only_file/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/bom_only_file/main.solc +--- +error[SC0001]: invalid token `` + --> /main/main.solc:1:1 + | +1 |  + | ^ invalid token diff --git a/crates/uitest/tests/fixtures/parse/bom_only_file/main.solc b/crates/uitest/tests/fixtures/parse/bom_only_file/main.solc new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/bom_only_file/main.solc @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/uitest/tests/fixtures/parse/class_missing_body_brace/diagnostics.snap b/crates/uitest/tests/fixtures/parse/class_missing_body_brace/diagnostics.snap new file mode 100644 index 00000000..ccd6c1f9 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/class_missing_body_brace/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/class_missing_body_brace/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:1:13 + | +1 | trait Eq + | ^ unexpected token + | + = note: expecting `{`, or where clause + = note: while parsing trait declaration diff --git a/crates/uitest/tests/fixtures/parse/class_missing_body_brace/main.solc b/crates/uitest/tests/fixtures/parse/class_missing_body_brace/main.solc new file mode 100644 index 00000000..a0835a2a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/class_missing_body_brace/main.solc @@ -0,0 +1 @@ +trait Eq diff --git a/crates/uitest/tests/fixtures/parse/data_trailing_pipe/diagnostics.snap b/crates/uitest/tests/fixtures/parse/data_trailing_pipe/diagnostics.snap new file mode 100644 index 00000000..9f4aff36 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/data_trailing_pipe/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/data_trailing_pipe/main.solc +--- +error[SC0001]: parse error: unexpected `,` + --> /main/main.solc:1:27 + | +1 | enum Option { Some(T), , }; + | ^ unexpected token + | + = note: expecting `}` + = note: while parsing enum or struct declaration diff --git a/crates/uitest/tests/fixtures/parse/data_trailing_pipe/main.solc b/crates/uitest/tests/fixtures/parse/data_trailing_pipe/main.solc new file mode 100644 index 00000000..21b867a6 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/data_trailing_pipe/main.solc @@ -0,0 +1 @@ +enum Option { Some(T), , }; diff --git a/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/diagnostics.snap b/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/diagnostics.snap new file mode 100644 index 00000000..369a8cbb --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/main.solc +--- +error[SC0001]: delimiter nesting exceeds the compiler limit of 128 + --> /main/main.solc:1:172 + | +1 | ...((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((... + | ^ diff --git a/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/main.solc b/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/main.solc new file mode 100644 index 00000000..5ff7c981 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/delimiter_nesting_limit/main.solc @@ -0,0 +1 @@ +function f(x: word) returns (word) { return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((x)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))); } diff --git a/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/diagnostics.snap new file mode 100644 index 00000000..ee3602e3 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/main.solc +--- +error[SC0001]: parse error: unexpected `(` + --> /main/main.solc:4:17 + | +3 | assembly { +4 | r := add(1, + | ^ unexpected token +5 | } + | + = note: expecting `revert` +--- + +error[SC0001]: parse error: unexpected `,` + --> /main/main.solc:4:19 + | +3 | assembly { +4 | r := add(1, + | ^ unexpected token +5 | } + | + = note: expecting `break`, `continue`, `for`, `function`, `if`, `leave`, `let`, `return`, `switch`, `{`, or assembly expression diff --git a/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/main.solc b/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/main.solc new file mode 100644 index 00000000..f11adb51 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_assembly_unclosed_call/main.solc @@ -0,0 +1,7 @@ +function f() returns (word) { + let r: word; + assembly { + r := add(1, + } + return r; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/diagnostics.snap new file mode 100644 index 00000000..58e4a018 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/main.solc +--- +error[SC0001]: parse error: unexpected `{` + --> /main/main.solc:1:10 + | +1 | contract { + | ^ unexpected token +2 | function f() returns (word) { +3 | return 1; + | + = note: while parsing contract, interface, or library declaration diff --git a/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/main.solc b/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/main.solc new file mode 100644 index 00000000..806d8334 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_contract_missing_name/main.solc @@ -0,0 +1,5 @@ +contract { + function f() returns (word) { + return 1; + } +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/diagnostics.snap new file mode 100644 index 00000000..30b7f2d0 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_function_missing_params/main.solc +--- +error[SC0001]: parse error: unexpected `returns` + --> /main/main.solc:1:12 + | +1 | function f returns (word) { + | ^^^^^^^ unexpected token +2 | return 1; +3 | } + | + = note: expecting `(`, or type parameter list + = note: keywords cannot be used as identifiers; choose a different name + = note: while parsing function signature diff --git a/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/main.solc b/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/main.solc new file mode 100644 index 00000000..3804e846 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_function_missing_params/main.solc @@ -0,0 +1,3 @@ +function f returns (word) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/diagnostics.snap new file mode 100644 index 00000000..47cd68fc --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/main.solc +--- +error[SC0001]: parse error: unexpected `}` + --> /main/main.solc:5:3 + | +4 | match (b) { +5 | } + | ^ unexpected token +6 | } + | + = note: expecting `case`, or `default` diff --git a/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/main.solc b/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/main.solc new file mode 100644 index 00000000..054a065d --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_hull_empty_match/main.solc @@ -0,0 +1,12 @@ +enum B { A, C }; + +function impossible(b: B) returns (word) { + match (b) { + } +} + +contract T { + function main(x: word) public returns (word) { + return impossible(B.A); + } +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/diagnostics.snap new file mode 100644 index 00000000..d2bcf2b1 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/main.solc +--- +error[SC0001]: fallback function must not declare input parameters + --> /main/main.solc:9:13 + | + 8 | + 9 | fallback(x: uint256) returns (()) { + | ^^^^^^^^^^^^ +10 | revert; + | + = note: while parsing fallback definition diff --git a/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/main.solc b/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/main.solc new file mode 100644 index 00000000..4041cf06 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_hull_fallback_args/main.solc @@ -0,0 +1,12 @@ +// Mirrors reference corpus test/examples/cases/fallback-with-args.solc +// (expected failure there): fallback must take no arguments. +import std; +import std.dispatch; + +contract BadFallback { + constructor() {} + + fallback(x: uint256) returns (()) { + revert; + } +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/diagnostics.snap new file mode 100644 index 00000000..9644c1b7 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/main.solc +--- +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:1:24 + | +1 | import * as b from a.b.; + | ^ unexpected token +2 | +3 | function f() returns (word) { + | + = note: while parsing import declaration diff --git a/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/main.solc b/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/main.solc new file mode 100644 index 00000000..dc31dc5a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_import_trailing_dot/main.solc @@ -0,0 +1,5 @@ +import * as b from a.b.; + +function f() returns (word) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/diagnostics.snap new file mode 100644 index 00000000..ebfe0964 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/main.solc +--- +error[SC0001]: invalid token `§` + --> /main/main.solc:2:15 + | +1 | function f() returns (word) { +2 | let x = 1 § 2; + | ^ invalid token +3 | return x; + | diff --git a/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/main.solc b/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/main.solc new file mode 100644 index 00000000..c7a88ee1 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_invalid_token_unicode/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let x = 1 § 2; + return x; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/diagnostics.snap new file mode 100644 index 00000000..b37a1673 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/main.solc +--- +error[SC0001]: parse error: unexpected `match` + --> /main/main.solc:1:10 + | +1 | function match(x: word) returns (word) { + | ^^^^^ unexpected token +2 | return x; +3 | } + | + = note: expecting identifier + = note: keywords cannot be used as identifiers; choose a different name + = note: while parsing function signature diff --git a/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/main.solc b/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/main.solc new file mode 100644 index 00000000..5cb9ee82 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_keyword_as_ident/main.solc @@ -0,0 +1,3 @@ +function match(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/diagnostics.snap new file mode 100644 index 00000000..443146c4 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/main.solc +--- +error[SC0001]: parse error: unexpected identifier `x` + --> /main/main.solc:2:17 + | +1 | function f() returns (word) { +2 | let g = lam x { return x; }; + | ^ unexpected token +3 | return g(1); + | + = note: expecting `(` +--- + +error[SC0001]: parse error: unexpected `}` + --> /main/main.solc:2:31 + | +1 | function f() returns (word) { +2 | let g = lam x { return x; }; + | ^ unexpected token +3 | return g(1); + | + = note: expecting end of input, or statement diff --git a/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/main.solc b/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/main.solc new file mode 100644 index 00000000..367eb2a5 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_lambda_missing_parens/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let g = lam x { return x; }; + return g(1); +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/diagnostics.snap new file mode 100644 index 00000000..5e1dcbe7 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/main.solc +--- +error[SC0001]: parse error: unexpected `return` + --> /main/main.solc:3:5 + | +2 | let x = 1 +3 | return x; + | ^^^^^^ unexpected token +4 | } + | + = note: expecting `;` after let statement diff --git a/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/main.solc b/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/main.solc new file mode 100644 index 00000000..e8c0ca0e --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_missing_semicolon_stmts/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let x = 1 + return x; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/diagnostics.snap new file mode 100644 index 00000000..75b8a8fd --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/main.solc +--- +error[SC0001]: parse error: unexpected `function` + --> /main/main.solc:3:1 + | +2 | +3 | function f() returns (word) { + | ^^^^^^^^ unexpected token +4 | return 1; + | + = note: expecting `;` + = note: while parsing pragma declaration diff --git a/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/main.solc b/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/main.solc new file mode 100644 index 00000000..e72df316 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_pragma_missing_semi/main.solc @@ -0,0 +1,5 @@ +pragma solcore noCoverageCondition + +function f() returns (word) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/diagnostics.snap new file mode 100644 index 00000000..f7fb038d --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/main.solc +--- +error[SC0001]: could not parse top-level item near `;`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /main/main.solc:3:2 + | +2 | return 1; +3 | }; + | ^ +4 | + | diff --git a/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/main.solc b/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/main.solc new file mode 100644 index 00000000..00006570 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_stray_top_level_semi/main.solc @@ -0,0 +1,7 @@ +function f() returns (word) { + return 1; +}; + +function g() returns (word) { + return 2; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/diagnostics.snap new file mode 100644 index 00000000..9f163e73 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/main.solc +--- +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:2:13 + | +1 | function f() returns (word) { +2 | let x = ; + | ^ unexpected token +3 | return 0; + | + = note: expecting expression after `=` +--- + +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:12:14 + | +11 | function h() returns (word) { +12 | return (1; + | ^ unexpected token +13 | } + | + = note: expecting `&&`, `&`, `(`, `)`, `*`, `,`, `.`, `<`, `>`, `?`, `[`, `^`, `as`, `|`, or `||` diff --git a/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/main.solc b/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/main.solc new file mode 100644 index 00000000..418b2486 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_two_errors_recovery/main.solc @@ -0,0 +1,13 @@ +function f() returns (word) { + let x = ; + return 0; +} + +function g(y: word) returns (word) { + if (y) { return 1; } + return 0; +} + +function h() returns (word) { + return (1; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/diagnostics.snap new file mode 100644 index 00000000..23eef3f0 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:4:7 + | +2 | function f() returns (word) { +3 | return 1; +4 | } + | ^ unexpected token + | + = note: expecting `}`, contract field, or contract member + = note: while parsing contract, interface, or library declaration diff --git a/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/main.solc b/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/main.solc new file mode 100644 index 00000000..ff5c44b8 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unclosed_brace_eof/main.solc @@ -0,0 +1,4 @@ +contract C { + function f() returns (word) { + return 1; + } diff --git a/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/diagnostics.snap new file mode 100644 index 00000000..ed33402e --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/main.solc +--- +error[SC0001]: unterminated block comment + --> /main/main.solc:4:1 + | +3 | } +4 | / /* this comment never ends +5 | | function g() returns (word) { +6 | | return 2; +7 | | } + | |__^ comment starts here + | + = note: add `*/` before the end of file diff --git a/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/main.solc b/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/main.solc new file mode 100644 index 00000000..a0b611a1 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unterminated_block_comment/main.solc @@ -0,0 +1,7 @@ +function f() returns (word) { + return 1; +} +/* this comment never ends +function g() returns (word) { + return 2; +} diff --git a/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/diagnostics.snap b/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/diagnostics.snap new file mode 100644 index 00000000..a8d268c7 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/ergo_unterminated_string/main.solc +--- +error[SC0001]: unterminated string literal + --> /main/main.solc:2:13 + | +1 | function f() returns (word) { +2 | let s = "hello; + | _____________^ +3 | | return 1; +4 | | } + | |__^ string literal starts here + | + = note: add a closing `"` before the end of file diff --git a/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/main.solc b/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/main.solc new file mode 100644 index 00000000..bb3e97a9 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/ergo_unterminated_string/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let s = "hello; + return 1; +} diff --git a/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/diagnostics.snap b/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/diagnostics.snap new file mode 100644 index 00000000..82b65e5a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/main.solc +--- +error[SC0001]: conditional expression nesting exceeds the compiler limit of 32 + --> /main/main.solc:35:22 + | +34 | true ? 0 : +35 | true ? 0 : + | ^ +36 | true ? 0 : + | diff --git a/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/main.solc b/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/main.solc new file mode 100644 index 00000000..b07678b8 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/excessive_conditional_nesting/main.solc @@ -0,0 +1,134 @@ +function main() returns (word) { + return + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + true ? 0 : + 0; +} diff --git a/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/diagnostics.snap b/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/diagnostics.snap new file mode 100644 index 00000000..db8ae7d7 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/excessive_expression_nesting/main.solc +--- +error[SC0001]: expression nesting exceeds the compiler limit of 32 + --> /main/main.solc:1:73 + | +1 | function main() returns (word) { return !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!true; } + | ^^^^^^^^^^^^ diff --git a/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/main.solc b/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/main.solc new file mode 100644 index 00000000..df75f702 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/excessive_expression_nesting/main.solc @@ -0,0 +1 @@ +function main() returns (word) { return !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!true; } diff --git a/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/diagnostics.snap b/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/diagnostics.snap new file mode 100644 index 00000000..f48f1e12 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/main.solc +--- +error[SC0001]: fallback function must return unit (`()`) + --> /main/main.solc:2:23 + | +1 | contract Bad { +2 | fallback() returns (word) {} + | ^^^^ +3 | + | + = note: while parsing fallback definition diff --git a/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/main.solc b/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/main.solc new file mode 100644 index 00000000..cd30dedd --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/fallback_with_non_unit_return/main.solc @@ -0,0 +1,5 @@ +contract Bad { + fallback() returns (word) {} + + function after() {} +} diff --git a/crates/uitest/tests/fixtures/parse/fallback_with_params/diagnostics.snap b/crates/uitest/tests/fixtures/parse/fallback_with_params/diagnostics.snap new file mode 100644 index 00000000..797ceb94 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/fallback_with_params/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/fallback_with_params/main.solc +--- +error[SC0001]: fallback function must not declare input parameters + --> /main/main.solc:2:11 + | +1 | contract Bad { +2 | fallback(x: word) {} + | ^^^^^^^^^ +3 | + | + = note: while parsing fallback definition diff --git a/crates/uitest/tests/fixtures/parse/fallback_with_params/main.solc b/crates/uitest/tests/fixtures/parse/fallback_with_params/main.solc new file mode 100644 index 00000000..e904dd28 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/fallback_with_params/main.solc @@ -0,0 +1,5 @@ +contract Bad { + fallback(x: word) {} + + function after() {} +} diff --git a/crates/uitest/tests/fixtures/parse/function_param_recovery/diagnostics.snap b/crates/uitest/tests/fixtures/parse/function_param_recovery/diagnostics.snap new file mode 100644 index 00000000..c82dc39a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/function_param_recovery/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/function_param_recovery/main.solc +--- +error[SC0001]: parse error: unexpected `,` + --> /main/main.solc:1:17 + | +1 | function bad(x: , y: U) {} + | ^ unexpected token +2 | function ok() {} + | + = note: expecting type + = note: while parsing function parameter diff --git a/crates/uitest/tests/fixtures/parse/function_param_recovery/main.solc b/crates/uitest/tests/fixtures/parse/function_param_recovery/main.solc new file mode 100644 index 00000000..b734109c --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/function_param_recovery/main.solc @@ -0,0 +1,2 @@ +function bad(x: , y: U) {} +function ok() {} diff --git a/crates/uitest/tests/fixtures/parse/function_signature_missing_type/diagnostics.snap b/crates/uitest/tests/fixtures/parse/function_signature_missing_type/diagnostics.snap new file mode 100644 index 00000000..6b10bc21 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/function_signature_missing_type/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/function_signature_missing_type/main.solc +--- +error[SC0001]: parse error: unexpected `)` + --> /main/main.solc:1:17 + | +1 | function bad(x: ) {} + | ^ unexpected token + | + = note: expecting type + = note: while parsing function parameter diff --git a/crates/parser/tests/fixtures/fail/function_signature_missing_type.solc b/crates/uitest/tests/fixtures/parse/function_signature_missing_type/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/function_signature_missing_type.solc rename to crates/uitest/tests/fixtures/parse/function_signature_missing_type/main.solc diff --git a/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/diagnostics.snap b/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/diagnostics.snap new file mode 100644 index 00000000..6e2e1be9 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/if_trailing_semicolon/main.solc +--- +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:4:4 + | +3 | return (); +4 | }; + | ^ unexpected token +5 | } + | + = note: expecting `else`, end of input, or statement diff --git a/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/main.solc b/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/main.solc new file mode 100644 index 00000000..caab80b6 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/if_trailing_semicolon/main.solc @@ -0,0 +1,5 @@ +function f() { + if (true) { + return (); + }; +} diff --git a/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/diagnostics.snap b/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/diagnostics.snap new file mode 100644 index 00000000..3caecec9 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/main.solc +--- +error[SC0001]: parse error: unexpected `(` + --> /main/main.solc:1:10 + | +1 | import {D(C)} from lib; + | ^ unexpected token + | + = note: expecting `,`, `as`, or `}` + = note: while parsing import declaration diff --git a/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/main.solc b/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/main.solc new file mode 100644 index 00000000..b1815643 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/import_ctor_group_syntax/main.solc @@ -0,0 +1 @@ +import {D(C)} from lib; diff --git a/crates/uitest/tests/fixtures/parse/import_selector_unterminated/diagnostics.snap b/crates/uitest/tests/fixtures/parse/import_selector_unterminated/diagnostics.snap new file mode 100644 index 00000000..189cda55 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/import_selector_unterminated/diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/import_selector_unterminated/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:1:10 + | +1 | import { + | ^ unexpected token + | + = note: while parsing import declaration diff --git a/crates/uitest/tests/fixtures/parse/import_selector_unterminated/main.solc b/crates/uitest/tests/fixtures/parse/import_selector_unterminated/main.solc new file mode 100644 index 00000000..c762184a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/import_selector_unterminated/main.solc @@ -0,0 +1 @@ +import { diff --git a/crates/uitest/tests/fixtures/parse/instance_missing_head/diagnostics.snap b/crates/uitest/tests/fixtures/parse/instance_missing_head/diagnostics.snap new file mode 100644 index 00000000..504513fc --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/instance_missing_head/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/instance_missing_head/main.solc +--- +error[SC0001]: parse error: unexpected `{` + --> /main/main.solc:1:6 + | +1 | impl {} + | ^ unexpected token + | + = note: expecting trait reference, or type parameter list + = note: while parsing impl declaration diff --git a/crates/uitest/tests/fixtures/parse/instance_missing_head/main.solc b/crates/uitest/tests/fixtures/parse/instance_missing_head/main.solc new file mode 100644 index 00000000..21261d48 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/instance_missing_head/main.solc @@ -0,0 +1 @@ +impl {} diff --git a/crates/uitest/tests/fixtures/parse/invalid_token/diagnostics.snap b/crates/uitest/tests/fixtures/parse/invalid_token/diagnostics.snap new file mode 100644 index 00000000..4cdc756c --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/invalid_token/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/invalid_token/main.solc +--- +error[SC0001]: invalid token `§` + --> /main/main.solc:1:1 + | +1 | § + | ^ invalid token diff --git a/crates/uitest/tests/fixtures/parse/invalid_token/main.solc b/crates/uitest/tests/fixtures/parse/invalid_token/main.solc new file mode 100644 index 00000000..3038d22f --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/invalid_token/main.solc @@ -0,0 +1 @@ +§ diff --git a/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/diagnostics.snap b/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/diagnostics.snap new file mode 100644 index 00000000..5f8d13b0 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/diagnostics.snap @@ -0,0 +1,22 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/main.solc +--- +error[SC0001]: parse error: unexpected `)` + --> /main/main.solc:1:20 + | +1 | function f(comptime) returns (word) { return comptime; } + | ^ unexpected token + | + = note: expecting identifier + = note: while parsing function parameter +--- + +error[SC0001]: parse error: unexpected `comptime` + --> /main/main.solc:1:46 + | +1 | function f(comptime) returns (word) { return comptime; } + | ^^^^^^^^ unexpected token + | + = note: expecting `!`, `(`, `@`, or `lam` diff --git a/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/main.solc b/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/main.solc new file mode 100644 index 00000000..e12b366c --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/keyword_comptime_identifier/main.solc @@ -0,0 +1 @@ +function f(comptime) returns (word) { return comptime; } diff --git a/crates/uitest/tests/fixtures/parse/missing_data_semicolon/diagnostics.snap b/crates/uitest/tests/fixtures/parse/missing_data_semicolon/diagnostics.snap new file mode 100644 index 00000000..8d1374cd --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/missing_data_semicolon/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/missing_data_semicolon/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:1:12 + | +1 | enum D { C + | ^ unexpected token + | + = note: expecting `(`, `,`, or `}` + = note: while parsing enum or struct declaration diff --git a/crates/uitest/tests/fixtures/parse/missing_data_semicolon/main.solc b/crates/uitest/tests/fixtures/parse/missing_data_semicolon/main.solc new file mode 100644 index 00000000..a1256e16 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/missing_data_semicolon/main.solc @@ -0,0 +1 @@ +enum D { C diff --git a/crates/uitest/tests/fixtures/parse/missing_semicolon/diagnostics.snap b/crates/uitest/tests/fixtures/parse/missing_semicolon/diagnostics.snap new file mode 100644 index 00000000..f0aabff1 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/missing_semicolon/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/missing_semicolon/main.solc +--- +error[SC0001]: parse error: unexpected end of input + --> /main/main.solc:1:18 + | +1 | import core.math + | ^ unexpected token + | + = note: expecting `.`, or `;` + = note: while parsing import declaration diff --git a/crates/parser/tests/fixtures/fail/missing_semicolon.solc b/crates/uitest/tests/fixtures/parse/missing_semicolon/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/missing_semicolon.solc rename to crates/uitest/tests/fixtures/parse/missing_semicolon/main.solc diff --git a/crates/uitest/tests/fixtures/parse/multibyte_eof_string/diagnostics.snap b/crates/uitest/tests/fixtures/parse/multibyte_eof_string/diagnostics.snap new file mode 100644 index 00000000..639abd9b --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/multibyte_eof_string/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/multibyte_eof_string/main.solc +--- +error[SC0001]: unterminated string literal + --> /main/main.solc:2:11 + | +1 | function f() returns (word) { +2 | let s = "café + | ^^^^^ string literal starts here + | + = note: add a closing `"` before the end of file diff --git a/crates/uitest/tests/fixtures/parse/multibyte_eof_string/main.solc b/crates/uitest/tests/fixtures/parse/multibyte_eof_string/main.solc new file mode 100644 index 00000000..b9627b80 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/multibyte_eof_string/main.solc @@ -0,0 +1,2 @@ +function f() returns (word) { + let s = "café \ No newline at end of file diff --git a/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/diagnostics.snap b/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/diagnostics.snap new file mode 100644 index 00000000..60fcc84a --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/diagnostics.snap @@ -0,0 +1,20 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/multiple_emitted_errors/main.solc +--- +error[SC0001]: invalid token `§` + --> /main/main.solc:1:1 + | +1 | § + | ^ invalid token +2 | # + | +--- + +error[SC0001]: invalid token `#` + --> /main/main.solc:2:1 + | +1 | § +2 | # + | ^ invalid token diff --git a/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/main.solc b/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/main.solc new file mode 100644 index 00000000..fadca976 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/multiple_emitted_errors/main.solc @@ -0,0 +1,2 @@ +§ +# diff --git a/crates/uitest/tests/fixtures/parse/multiple_errors_continue/diagnostics.snap b/crates/uitest/tests/fixtures/parse/multiple_errors_continue/diagnostics.snap new file mode 100644 index 00000000..5b65fafd --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/multiple_errors_continue/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/multiple_errors_continue/main.solc +--- +error[SC0001]: import declaration requires trailing `;` + --> /main/main.solc:2:1 + | +1 | import core.math +2 | function bad() { + | ^^^^^^^^ +3 | let x = ; + | + = note: while parsing import declaration +--- + +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:3:13 + | +2 | function bad() { +3 | let x = ; + | ^ unexpected token +4 | return 1; + | + = note: expecting expression after `=` diff --git a/crates/parser/tests/fixtures/fail/multiple_errors_continue.solc b/crates/uitest/tests/fixtures/parse/multiple_errors_continue/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/multiple_errors_continue.solc rename to crates/uitest/tests/fixtures/parse/multiple_errors_continue/main.solc diff --git a/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/diagnostics.snap b/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/diagnostics.snap new file mode 100644 index 00000000..dad4505f --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/main.solc +--- +error[SC0001]: parse error: unexpected `)` + --> /main/main.solc:5:12 + | +4 | match (x) { +5 | case D.C() { return 1; } + | ^ unexpected token +6 | } + | + = note: expecting `(`, `_`, or `comptime` diff --git a/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/main.solc b/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/main.solc new file mode 100644 index 00000000..fb2ad61e --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/nullary_ctor_applied_pattern/main.solc @@ -0,0 +1,7 @@ +enum D { C }; + +function f(x: D) returns (word) { + match (x) { + case D.C() { return 1; } + } +} diff --git a/crates/uitest/tests/fixtures/parse/pragma_missing_name/diagnostics.snap b/crates/uitest/tests/fixtures/parse/pragma_missing_name/diagnostics.snap new file mode 100644 index 00000000..3dc5c31b --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/pragma_missing_name/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/pragma_missing_name/main.solc +--- +error[SC0001]: parse error: unexpected `;` + --> /main/main.solc:1:8 + | +1 | pragma ; + | ^ unexpected token + | + = note: expecting solcore, or solidity or abicoder + = note: while parsing pragma declaration diff --git a/crates/parser/tests/fixtures/fail/pragma_missing_name.solc b/crates/uitest/tests/fixtures/parse/pragma_missing_name/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/pragma_missing_name.solc rename to crates/uitest/tests/fixtures/parse/pragma_missing_name/main.solc diff --git a/crates/uitest/tests/fixtures/parse/public_constructor/diagnostics.snap b/crates/uitest/tests/fixtures/parse/public_constructor/diagnostics.snap new file mode 100644 index 00000000..430ce59c --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_constructor/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/public_constructor/main.solc +--- +error[SC0001]: constructor is implicitly public; remove the visibility keyword + --> /main/main.solc:2:17 + | +1 | contract Bad { +2 | constructor() public {} + | ^^^^^^ +3 | + | + = note: while parsing constructor definition diff --git a/crates/uitest/tests/fixtures/parse/public_constructor/main.solc b/crates/uitest/tests/fixtures/parse/public_constructor/main.solc new file mode 100644 index 00000000..9bc248c4 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_constructor/main.solc @@ -0,0 +1,5 @@ +contract Bad { + constructor() public {} + + function after() {} +} diff --git a/crates/uitest/tests/fixtures/parse/public_fallback/diagnostics.snap b/crates/uitest/tests/fixtures/parse/public_fallback/diagnostics.snap new file mode 100644 index 00000000..8b1cc7cb --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_fallback/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/public_fallback/main.solc +--- +error[SC0001]: `public` is not allowed on fallback; use `external` + --> /main/main.solc:2:14 + | +1 | contract Bad { +2 | fallback() public {} + | ^^^^^^ +3 | + | + = note: while parsing fallback definition diff --git a/crates/uitest/tests/fixtures/parse/public_fallback/main.solc b/crates/uitest/tests/fixtures/parse/public_fallback/main.solc new file mode 100644 index 00000000..6ec041f2 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_fallback/main.solc @@ -0,0 +1,5 @@ +contract Bad { + fallback() public {} + + function after() {} +} diff --git a/crates/uitest/tests/fixtures/parse/public_free_function/diagnostics.snap b/crates/uitest/tests/fixtures/parse/public_free_function/diagnostics.snap new file mode 100644 index 00000000..1053f4f3 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_free_function/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/public_free_function/main.solc +--- +error[SC0001]: could not parse top-level item near `public`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /main/main.solc:2:1 + | +1 | // migrate-syntax: keep-legacy-negative +2 | public function bad() {} + | ^^^^^^ +3 | + | diff --git a/crates/uitest/tests/fixtures/parse/public_free_function/main.solc b/crates/uitest/tests/fixtures/parse/public_free_function/main.solc new file mode 100644 index 00000000..6ff3c7e4 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/public_free_function/main.solc @@ -0,0 +1,4 @@ +// migrate-syntax: keep-legacy-negative +public function bad() {} + +function after() {} diff --git a/crates/uitest/tests/fixtures/parse/string_bad_escape/diagnostics.snap b/crates/uitest/tests/fixtures/parse/string_bad_escape/diagnostics.snap new file mode 100644 index 00000000..448ad3e9 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/string_bad_escape/diagnostics.snap @@ -0,0 +1,10 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/string_bad_escape/main.solc +--- +error[SC0001]: invalid string escape `/q` + --> /main/main.solc:1:40 + | +1 | function f() returns (string) { return "a/q"; } + | ^^^^^ invalid escape sequence diff --git a/crates/uitest/tests/fixtures/parse/string_bad_escape/main.solc b/crates/uitest/tests/fixtures/parse/string_bad_escape/main.solc new file mode 100644 index 00000000..5de7e713 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/string_bad_escape/main.solc @@ -0,0 +1 @@ +function f() returns (string) { return "a\q"; } diff --git a/crates/uitest/tests/fixtures/parse/top_level_recovery/diagnostics.snap b/crates/uitest/tests/fixtures/parse/top_level_recovery/diagnostics.snap new file mode 100644 index 00000000..eb375950 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/top_level_recovery/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/top_level_recovery/main.solc +--- +error[SC0001]: could not parse top-level item near `unknown nonsense tokens`; expected a declaration starting with `import`, `pragma`, `type`, `alias`, `enum`, `struct`, `trait`, `impl`, `contract`, `interface`, `library`, or `function` + --> /main/main.solc:2:1 + | +1 | function first() {} +2 | unknown nonsense tokens + | ^^^^^^^^^^^^^^^^^^^^^^^ +3 | function second() {} + | diff --git a/crates/parser/tests/fixtures/fail/top_level_recovery.solc b/crates/uitest/tests/fixtures/parse/top_level_recovery/main.solc similarity index 100% rename from crates/parser/tests/fixtures/fail/top_level_recovery.solc rename to crates/uitest/tests/fixtures/parse/top_level_recovery/main.solc diff --git a/crates/uitest/tests/fixtures/parse/trailing_call_comma/diagnostics.snap b/crates/uitest/tests/fixtures/parse/trailing_call_comma/diagnostics.snap new file mode 100644 index 00000000..3343cd71 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_call_comma/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/trailing_call_comma/main.solc +--- +error[SC0001]: parse error: unexpected `)` + --> /main/main.solc:2:42 + | +1 | function g(x: word) returns (word) { return x; } +2 | function f() returns (word) { return g(1,); } + | ^ unexpected token + | + = note: expecting `!`, `(`, `@`, or `lam` diff --git a/crates/uitest/tests/fixtures/parse/trailing_call_comma/main.solc b/crates/uitest/tests/fixtures/parse/trailing_call_comma/main.solc new file mode 100644 index 00000000..9cf897f6 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_call_comma/main.solc @@ -0,0 +1,2 @@ +function g(x: word) returns (word) { return x; } +function f() returns (word) { return g(1,); } diff --git a/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/diagnostics.snap b/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/diagnostics.snap new file mode 100644 index 00000000..9aabaf20 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/trailing_constructor_comma/main.solc +--- +error[SC0001]: parse error: unexpected `,` + --> /main/main.solc:1:17 + | +1 | enum D { C(word,,) }; + | ^ unexpected token + | + = note: expecting `)`, or type + = note: while parsing enum or struct declaration diff --git a/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/main.solc b/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/main.solc new file mode 100644 index 00000000..06effa8b --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_constructor_comma/main.solc @@ -0,0 +1 @@ +enum D { C(word,,) }; diff --git a/crates/uitest/tests/fixtures/parse/trailing_import_comma/diagnostics.snap b/crates/uitest/tests/fixtures/parse/trailing_import_comma/diagnostics.snap new file mode 100644 index 00000000..9e9eb95d --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_import_comma/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/trailing_import_comma/main.solc +--- +error[SC0001]: parse error: unexpected `,` + --> /main/main.solc:1:14 + | +1 | import {a, b,,} from m; + | ^ unexpected token + | + = note: expecting `}` + = note: while parsing import declaration diff --git a/crates/uitest/tests/fixtures/parse/trailing_import_comma/main.solc b/crates/uitest/tests/fixtures/parse/trailing_import_comma/main.solc new file mode 100644 index 00000000..837e2dd5 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/trailing_import_comma/main.solc @@ -0,0 +1 @@ +import {a, b,,} from m; diff --git a/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/diagnostics.snap b/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/diagnostics.snap new file mode 100644 index 00000000..c1289f5b --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/parse/type_alias_missing_equals/main.solc +--- +error[SC0001]: parse error: unexpected identifier `U` + --> /main/main.solc:1:14 + | +1 | alias Amount U; + | ^ unexpected token + | + = note: expecting `=`, or type parameter list + = note: while parsing type declaration diff --git a/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/main.solc b/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/main.solc new file mode 100644 index 00000000..6cd1ed17 --- /dev/null +++ b/crates/uitest/tests/fixtures/parse/type_alias_missing_equals/main.solc @@ -0,0 +1 @@ +alias Amount U; diff --git a/crates/uitest/tests/fixtures/solver/bounded_variable_condition/diagnostics.snap b/crates/uitest/tests/fixtures/solver/bounded_variable_condition/diagnostics.snap new file mode 100644 index 00000000..9894087e --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/bounded_variable_condition/diagnostics.snap @@ -0,0 +1,12 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/bounded_variable_condition/main.solc +--- +error[SC0214]: Bounded variable condition fails! + --> /main/main.solc:5:12 + | +3 | trait Container {} +4 | +5 | impl Container, a> where c: Eq {} + | ^^^^^^^^^^^^^^^^^^^^ impl head is missing constraint variables diff --git a/crates/uitest/tests/fixtures/solver/bounded_variable_condition/main.solc b/crates/uitest/tests/fixtures/solver/bounded_variable_condition/main.solc new file mode 100644 index 00000000..6e94d4ee --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/bounded_variable_condition/main.solc @@ -0,0 +1,5 @@ +enum Box { Box(word) }; +trait Eq {} +trait Container {} + +impl Container, a> where c: Eq {} diff --git a/crates/uitest/tests/fixtures/solver/coverage_condition/diagnostics.snap b/crates/uitest/tests/fixtures/solver/coverage_condition/diagnostics.snap new file mode 100644 index 00000000..2df0aa0c --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/coverage_condition/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/coverage_condition/main.solc +--- +error[SC0212]: Coverage condition fails for trait: + MyClass + - the type: + Box + does not determine: + b + --> /main/main.solc:4:12 + | +2 | trait MyClass {} +3 | +4 | impl MyClass, b> {} + | ^^^^^^^^^^^^^^^^^^ impl head does not determine these variables diff --git a/crates/uitest/tests/fixtures/solver/coverage_condition/main.solc b/crates/uitest/tests/fixtures/solver/coverage_condition/main.solc new file mode 100644 index 00000000..7f577411 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/coverage_condition/main.solc @@ -0,0 +1,4 @@ +enum Box { Box(word) }; +trait MyClass {} + +impl MyClass, b> {} diff --git a/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/diagnostics.snap b/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/diagnostics.snap new file mode 100644 index 00000000..21fbdc89 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/main.solc +--- +error[SC0212]: Coverage condition fails for trait: + MyClass + - the type: + word + does not determine: + a + --> /main/main.solc:4:9 + | +2 | trait MyClass {} +3 | +4 | impl MyClass, a> {} + | ^^^^^^^^^^^^^^^^^^^^^^ impl head does not determine these variables diff --git a/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/main.solc b/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/main.solc new file mode 100644 index 00000000..337fd611 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/coverage_condition_alias_expansion/main.solc @@ -0,0 +1,4 @@ +alias Phantom = word; +trait MyClass {} + +impl MyClass, a> {} diff --git a/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/diagnostics.snap new file mode 100644 index 00000000..2d262417 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/main.solc +--- +error[SC0299]: ambiguous inferred type + --> /main/main.solc:28:29 + | +27 | +28 | function f() returns (word) { + | _____________________________^ +29 | | return Conv.out(Conv.make(1)); +30 | | } + | |_^ ambiguous inferred type + | + = note: <_> function() returns (word) where _: Conv + = help: add a type annotation or a matching impl to fix the ambiguous type variable diff --git a/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/main.solc b/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/main.solc new file mode 100644 index 00000000..14d218fa --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_ambiguous_defaulting/main.solc @@ -0,0 +1,30 @@ +enum Wrap { Wrap(word) }; + +trait Conv { + function make(x: word) returns (a); + function out(y: a) returns (word); +} + +impl Conv { + function make(x: word) returns (word) { + return x; + } + function out(y: word) returns (word) { + return y; + } +} + +impl Conv { + function make(x: word) returns (Wrap) { + return Wrap.Wrap(x); + } + function out(y: Wrap) returns (word) { + match (y) { + case Wrap.Wrap(w) { return w; } + } + } +} + +function f() returns (word) { + return Conv.out(Conv.make(1)); +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/diagnostics.snap new file mode 100644 index 00000000..b390e563 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_constraint_escape/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: a: Same + --> /main/main.solc:7:12 + | +6 | +7 | function f(x: a) returns (Bool) { + | ^ constraint originates here +8 | return Same.same(x, x); + | + = note: no visible impl matches `a: Same` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/main.solc b/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/main.solc new file mode 100644 index 00000000..302cf0a4 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_constraint_escape/main.solc @@ -0,0 +1,9 @@ +enum Bool { True, False }; + +trait Same { + function same(x: a, y: a) returns (Bool); +} + +function f(x: a) returns (Bool) { + return Same.same(x, x); +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/diagnostics.snap new file mode 100644 index 00000000..85d6e3bc --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: word: Eq + --> /main/main.solc:9:12 + | + 8 | function go(x: word) returns (Bool) { + 9 | return Eq.eq(x, x); + | ^^^^^^^^^^^ constraint originates here +10 | } + | + = note: no visible impl matches `word: Eq` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/main.solc b/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/main.solc new file mode 100644 index 00000000..75dd2bab --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_contract_no_instance/main.solc @@ -0,0 +1,13 @@ +enum Bool { True, False }; + +trait Eq { + function eq(x: a, y: a) returns (Bool); +} + +contract Check { + function go(x: word) returns (Bool) { + return Eq.eq(x, x); + } + + function main() returns (()) {} +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/diagnostics.snap new file mode 100644 index 00000000..4b5d64d4 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/main.solc +--- +error[SC0209]: cannot solve trait constraint `word: C`: solver exceeded its iteration bound + --> /main/main.solc:16:10 + | +15 | function f() returns (word) { +16 | return C.c(0); + | ^^^^^^ constraint originates here +17 | } + | + = help: simplify the impl chain or add a more direct impl diff --git a/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/main.solc b/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/main.solc new file mode 100644 index 00000000..db146845 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_fuel_blowup/main.solc @@ -0,0 +1,17 @@ +pragma solcore noPattersonCondition ; + +enum Box { MkBox(a) }; + +trait C { + function c(x: a) returns (word); +} + +impl C where Box: C { + function c(x: a) returns (word) { + return 1; + } +} + +function f() returns (word) { + return C.c(0); +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/diagnostics.snap new file mode 100644 index 00000000..a267ac6a --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/main.solc +--- +error[SC0217]: trait arity mismatch for `Rel`: expected 1, got 0 + --> /main/main.solc:5:6 + | +4 | +5 | impl Rel { + | ^^^^^^^^^ trait constraint arity mismatch +6 | function rel(x: word, y: word) returns (word) { + | diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/main.solc b/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/main.solc new file mode 100644 index 00000000..38a1856d --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_class_arity/main.solc @@ -0,0 +1,9 @@ +trait Rel { + function rel(x: a, y: b) returns (word); +} + +impl Rel { + function rel(x: word, y: word) returns (word) { + return 1; + } +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/diagnostics.snap new file mode 100644 index 00000000..a71e7c0a --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/main.solc +--- +error[SC0221]: invalid impl member signature for `size`: expected function(Bool) returns (word), got function(Bool) returns (Bool) + --> /main/main.solc:8:3 + | +7 | impl Sz { +8 | function size(x: Bool) returns (Bool) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ invalid impl method signature +9 | return x; + | + = note: the impl method must match the trait method after substituting the impl head diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/main.solc b/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/main.solc new file mode 100644 index 00000000..0924de44 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_method_sig_mismatch/main.solc @@ -0,0 +1,11 @@ +enum Bool { True, False }; + +trait Sz { + function size(x: a) returns (word); +} + +impl Sz { + function size(x: Bool) returns (Bool) { + return x; + } +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/diagnostics.snap new file mode 100644 index 00000000..9bbd4e3d --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/main.solc +--- +error[SC0299]: Invalid number of type arguments! + --> /main/main.solc:5:8 + | +3 | trait C {} +4 | +5 | impl C {} + | ^^^ diagnostic reported here + | + = note: Type Box is expected to have 1 type arguments + = note: but, type Box has 0 arguments diff --git a/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/main.solc b/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/main.solc new file mode 100644 index 00000000..db37319c --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_inst_wrong_kind/main.solc @@ -0,0 +1,5 @@ +enum Box { Box(a) }; + +trait C {} + +impl C {} diff --git a/crates/uitest/tests/fixtures/solver/ergo_no_instance/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_no_instance/diagnostics.snap new file mode 100644 index 00000000..105eec87 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_no_instance/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_no_instance/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: Bool: Eq + --> /main/main.solc:14:10 + | +13 | function f() returns (Bool) { +14 | return Eq.eq(Bool.True, Bool.False); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constraint originates here +15 | } + | + = note: no visible impl matches `Bool: Eq` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/solver/ergo_no_instance/main.solc b/crates/uitest/tests/fixtures/solver/ergo_no_instance/main.solc new file mode 100644 index 00000000..dc762c44 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_no_instance/main.solc @@ -0,0 +1,15 @@ +enum Bool { True, False }; + +trait Eq { + function eq(x: a, y: a) returns (Bool); +} + +impl Eq { + function eq(x: word, y: word) returns (Bool) { + return Bool.True; + } +} + +function f() returns (Bool) { + return Eq.eq(Bool.True, Bool.False); +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/diagnostics.snap new file mode 100644 index 00000000..3651439b --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/diagnostics.snap @@ -0,0 +1,22 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/main.solc +--- +error[SC0218]: Overlapping impls are not supported + impl: + word: C + overlaps with: + word: C + --> /main/main.solc:11:6 + | + 4 | + 5 | impl C { + | ------- previous overlapping impl + 6 | function c(x: word) returns (word) { +... +10 | +11 | impl C { + | ^^^^^^^ overlapping impl +12 | function c(x: word) returns (word) { + | diff --git a/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/main.solc b/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/main.solc new file mode 100644 index 00000000..6a63612d --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_overlapping_instances/main.solc @@ -0,0 +1,19 @@ +trait C { + function c(x: a) returns (word); +} + +impl C { + function c(x: word) returns (word) { + return 1; + } +} + +impl C { + function c(x: word) returns (word) { + return 2; + } +} + +function f() returns (word) { + return C.c(0); +} diff --git a/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/diagnostics.snap b/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/diagnostics.snap new file mode 100644 index 00000000..0381bb2c --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/ergo_patterson_violation/main.solc +--- +error[SC0213]: impl `U: C1` does not satisfy the Patterson conditions + --> /main/main.solc:4:9 + | +2 | trait C2 {} +3 | +4 | impl C1 where U: C1, U: C2 {} + | ^^^^^ impl head violates Patterson condition + | + = note: each impl constraint must be structurally smaller than the impl head + = help: remove the recursive constraint, add a more specific impl, or use the Patterson-condition pragma intentionally diff --git a/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/main.solc b/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/main.solc new file mode 100644 index 00000000..772f9d7f --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/ergo_patterson_violation/main.solc @@ -0,0 +1,4 @@ +trait C1 {} +trait C2 {} + +impl C1 where U: C1, U: C2 {} diff --git a/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/diagnostics.snap b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/diagnostics.snap new file mode 100644 index 00000000..daaa4e90 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/diagnostics.snap @@ -0,0 +1,44 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/main.solc +--- +error[SC0212]: Coverage condition fails for trait: + C + - the type: + List + does not determine: + a + --> /main/main.solc:7:12 + | +6 | +7 | impl C, a, List> {} + | ^^^^^^^^^^^^^^^^^^^^^^ impl head does not determine these variables +8 | impl C where x: C {} + | +--- + +error[SC0213]: impl `x: C` does not satisfy the Patterson conditions + --> /main/main.solc:8:9 + | +6 | +7 | impl C, a, List> {} +8 | impl C where x: C {} + | ^^^^^^^^^^^^^^^^ impl head violates Patterson condition + | + = note: each impl constraint must be structurally smaller than the impl head + = help: remove the recursive constraint, add a more specific impl, or use the Patterson-condition pragma intentionally +--- + +error[SC0218]: Overlapping impls are not supported + impl: + x: C + overlaps with: + List<_>: C<_, List<_>> + --> /main/main.solc:8:9 + | +6 | +7 | impl C, a, List> {} + | ---------------------- previous overlapping impl +8 | impl C where x: C {} + | ^^^^^^^^^^^^^^^^ overlapping impl diff --git a/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/main.solc b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/main.solc new file mode 100644 index 00000000..17fb934b --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/main.solc @@ -0,0 +1,8 @@ +import * as pragma_scope_lib from pragma_scope_lib; + +enum List { Nil, Cons(a, List) }; + +trait C {} + +impl C, a, List> {} +impl C where x: C {} diff --git a/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/pragma_scope_lib.solc b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/pragma_scope_lib.solc new file mode 100644 index 00000000..70140bbe --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/imported_pragma_does_not_suppress_local/pragma_scope_lib.solc @@ -0,0 +1,7 @@ +export { helper }; + +pragma solcore noPattersonCondition C; + +function helper() returns (word) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/solver/instance_extra_method/diagnostics.snap b/crates/uitest/tests/fixtures/solver/instance_extra_method/diagnostics.snap new file mode 100644 index 00000000..e3404a7d --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/instance_extra_method/diagnostics.snap @@ -0,0 +1,18 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/instance_extra_method/main.solc +--- +error[SC0202]: undefined name: C.g + --> /main/main.solc:7:12 + | +1 | trait C { + | - trait defined here +2 | function f(x: a) returns (word); +3 | } +... +6 | function f(x: word) returns (word) { return x; } +7 | function g(x: word) returns (word) { return x; } + | ^ unknown name +8 | } + | diff --git a/crates/uitest/tests/fixtures/solver/instance_extra_method/main.solc b/crates/uitest/tests/fixtures/solver/instance_extra_method/main.solc new file mode 100644 index 00000000..097bf54f --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/instance_extra_method/main.solc @@ -0,0 +1,8 @@ +trait C { + function f(x: a) returns (word); +} + +impl C { + function f(x: word) returns (word) { return x; } + function g(x: word) returns (word) { return x; } +} diff --git a/crates/uitest/tests/fixtures/solver/invalid_default_instance/diagnostics.snap b/crates/uitest/tests/fixtures/solver/invalid_default_instance/diagnostics.snap new file mode 100644 index 00000000..65b46d54 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/invalid_default_instance/diagnostics.snap @@ -0,0 +1,11 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/invalid_default_instance/main.solc +--- +error[SC0219]: Cannot have a default impl whose main argument contains no type variable: word: C + --> /main/main.solc:2:14 + | +1 | trait C {} +2 | default impl C {} + | ^^^^^^^ invalid default impl head diff --git a/crates/uitest/tests/fixtures/solver/invalid_default_instance/main.solc b/crates/uitest/tests/fixtures/solver/invalid_default_instance/main.solc new file mode 100644 index 00000000..8f959b16 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/invalid_default_instance/main.solc @@ -0,0 +1,2 @@ +trait C {} +default impl C {} diff --git a/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/diagnostics.snap b/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/diagnostics.snap new file mode 100644 index 00000000..cba48e57 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: word: C + --> /main/main.solc:6:10 + | +5 | function bad() returns (word) where a: C { +6 | return C.c(1); + | ^^^^^^ constraint originates here +7 | } + | + = note: no visible impl matches `word: C` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/main.solc b/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/main.solc new file mode 100644 index 00000000..d8f74b0c --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/local_given_rigid_var_unsatisfied/main.solc @@ -0,0 +1,7 @@ +trait C { + function c(x: a) returns (word); +} + +function bad() returns (word) where a: C { + return C.c(1); +} diff --git a/crates/uitest/tests/fixtures/solver/method_extra_forall/diagnostics.snap b/crates/uitest/tests/fixtures/solver/method_extra_forall/diagnostics.snap new file mode 100644 index 00000000..ade0251d --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/method_extra_forall/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/method_extra_forall/main.solc +--- +error[SC0299]: ambiguous inferred type + --> /main/main.solc:7:6 + | +6 | +7 | impl C { + | ^^^^^^^ ambiguous inferred type +8 | function f(x: word) returns (word) where b: D { return x; } + | + = note: function(word) returns (word) where b: D + = help: add a type annotation or a matching impl to fix the ambiguous type variable diff --git a/crates/uitest/tests/fixtures/solver/method_extra_forall/main.solc b/crates/uitest/tests/fixtures/solver/method_extra_forall/main.solc new file mode 100644 index 00000000..231e8a2f --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/method_extra_forall/main.solc @@ -0,0 +1,9 @@ +trait C { + function f(x: a) returns (word); +} + +trait D {} + +impl C { + function f(x: word) returns (word) where b: D { return x; } +} diff --git a/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/diagnostics.snap b/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/diagnostics.snap new file mode 100644 index 00000000..f9f2f07b --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/non_ground_unique_answer/main.solc +--- +error[SC0208]: ambiguous trait constraint: word: Parent + --> /main/main.solc:12:10 + | +11 | function trigger() returns (word) { +12 | return use(0); + | ^^^^^^ ambiguous constraint here +13 | } + | + = note: the matching proof leaves existential type variables unresolved + = help: make the type more specific or remove overlapping impls diff --git a/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/main.solc b/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/main.solc new file mode 100644 index 00000000..d3105df7 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/non_ground_unique_answer/main.solc @@ -0,0 +1,17 @@ +pragma solcore noCoverageCondition; + +trait Parent {} +trait Child where a: Parent {} +impl Child {} + +function use(x: a) returns (a) where a: Parent { + return x; +} + +function trigger() returns (word) { + return use(0); +} + +function main() returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/diagnostics.snap b/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/diagnostics.snap new file mode 100644 index 00000000..727c3c90 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/main.solc +--- +error[SC0206]: non-callable value of type word + --> /main/main.solc:3:10 + | +2 | let x: word = 1; +3 | return x(); + | ^ callee is not callable +4 | } + | diff --git a/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/main.solc b/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/main.solc new file mode 100644 index 00000000..9acaf427 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/noncallable_invokable_constraint/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let x: word = 1; + return x(); +} diff --git a/crates/uitest/tests/fixtures/solver/patterson_condition/diagnostics.snap b/crates/uitest/tests/fixtures/solver/patterson_condition/diagnostics.snap new file mode 100644 index 00000000..9eb93188 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/patterson_condition/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/patterson_condition/main.solc +--- +error[SC0213]: impl `U: C1` does not satisfy the Patterson conditions + --> /main/main.solc:4:9 + | +2 | trait C2 {} +3 | +4 | impl C1 where U: C1, U: C2 {} + | ^^^^^ impl head violates Patterson condition + | + = note: each impl constraint must be structurally smaller than the impl head + = help: remove the recursive constraint, add a more specific impl, or use the Patterson-condition pragma intentionally diff --git a/crates/uitest/tests/fixtures/solver/patterson_condition/main.solc b/crates/uitest/tests/fixtures/solver/patterson_condition/main.solc new file mode 100644 index 00000000..772f9d7f --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/patterson_condition/main.solc @@ -0,0 +1,4 @@ +trait C1 {} +trait C2 {} + +impl C1 where U: C1, U: C2 {} diff --git a/crates/uitest/tests/fixtures/solver/poly_int_defaulting/diagnostics.snap b/crates/uitest/tests/fixtures/solver/poly_int_defaulting/diagnostics.snap new file mode 100644 index 00000000..9d6e65c2 --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/poly_int_defaulting/diagnostics.snap @@ -0,0 +1,18 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/solver/poly_int_defaulting/main.solc +--- +error[SC0299]: ambiguous inferred type + --> /main/main.solc:5:29 + | +4 | +5 | function f() returns (word) { + | _____________________________^ +6 | | let y = poly(7); +7 | | return 0; +8 | | } + | |_^ ambiguous inferred type + | + = note: <_> function() returns (word) where _: Int + = help: add a type annotation or a matching impl to fix the ambiguous type variable diff --git a/crates/uitest/tests/fixtures/solver/poly_int_defaulting/main.solc b/crates/uitest/tests/fixtures/solver/poly_int_defaulting/main.solc new file mode 100644 index 00000000..5d1bee2b --- /dev/null +++ b/crates/uitest/tests/fixtures/solver/poly_int_defaulting/main.solc @@ -0,0 +1,8 @@ +function poly(x: a) returns (a) where a: Int { + return x; +} + +function f() returns (word) { + let y = poly(7); + return 0; +} diff --git a/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/diagnostics.snap new file mode 100644 index 00000000..51198f6c --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/main.solc +--- +error[SC0409]: comptime evaluation failed: comptime let 'y' is bound to a runtime expression + --> /main/main.solc:11:5 + | +10 | function main() public returns (word) { +11 | let comptime y: word = sloadWord(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +12 | return y; + | diff --git a/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/main.solc b/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/main.solc new file mode 100644 index 00000000..c3b503d4 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/comptime_evaluation_failed/main.solc @@ -0,0 +1,14 @@ +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract C { + function main() public returns (word) { + let comptime y: word = sloadWord(); + return y; + } +} diff --git a/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/diagnostics.snap new file mode 100644 index 00000000..ec90538b --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/main.solc +--- +error[SC0409]: comptime evaluation failed: function with a comptime return type returns a runtime expression + --> /main/main.solc:10:3 + | + 9 | function leak(comptime x: word) returns (comptime word) { +10 | return sloadWord(); + | ^^^^^^^^^^^^^^^^^^^ comptime evaluation failed here +11 | } + | diff --git a/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/main.solc b/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/main.solc new file mode 100644 index 00000000..a41b7205 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/comptime_return_evaluation_failed/main.solc @@ -0,0 +1,17 @@ +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +function leak(comptime x: word) returns (comptime word) { + return sloadWord(); +} + +contract C { + function main() public returns (word) { + return leak(1); + } +} diff --git a/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/diagnostics.snap new file mode 100644 index 00000000..355a3d97 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/main.solc +--- +error[SC0413]: externally visible function `double` cannot take comptime parameter `x` + --> /main/main.solc:9:19 + | + 8 | contract CtPublicParam { + 9 | function double(comptime x: word) public returns (word) { + | ^^^^^^^^^^^^^^^^ external ABI parameter is runtime +10 | return x + x; + | + = note: externally visible function parameters are supplied from calldata at runtime + = note: help: remove `comptime` from the ABI parameter or call a private comptime helper with a compile-time value diff --git a/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/main.solc b/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/main.solc new file mode 100644 index 00000000..6b26796b --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_ct_public_param/main.solc @@ -0,0 +1,12 @@ +// comptime parameter on a *public* contract entry point. Public entry +// arguments come from calldata at runtime, so this can never be satisfied. +// Should be rejected with a clear "public functions cannot take comptime +// parameters" style error. +import std; +import std.dispatch; + +contract CtPublicParam { + function double(comptime x: word) public returns (word) { + return x + x; + } +} diff --git a/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/diagnostics.snap new file mode 100644 index 00000000..0b80a0fa --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/main.solc +--- +error[SC0401]: cannot specialize expression: unresolved type parameter in Option(_) + --> /main/main.solc:10:13 + | + 9 | function main() returns (word) { +10 | let x = Option.None; + | ^^^^^^^^^^^ type must be concrete here +11 | return 1; + | + = note: this can happen when a constructor or expression leaves a type parameter unresolved + = note: help: add a type annotation that fixes the concrete type diff --git a/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/main.solc b/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/main.solc new file mode 100644 index 00000000..7217182f --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_free_tyvar_ctor/main.solc @@ -0,0 +1,13 @@ +// Unconstrained constructor: the type argument of Option is never fixed, +// so specialization sees a free type variable. Judge whether the error +// points at `None` and names the type variable usefully. +import * as std from std; + +enum Option { None, Some(a) }; + +contract FreeTyVarCtor { + function main() returns (word) { + let x = Option.None; + return 1; + } +} diff --git a/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/diagnostics.snap new file mode 100644 index 00000000..0a177ff5 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/main.solc +--- +error[SC0401]: cannot specialize expression: type is not concrete + --> /main/main.solc:15:13 + | +14 | let b: Box = Box.MkBox(1); +15 | if (v > 0) { + | ^ type must be concrete here +16 | b = Box.MkBox(2); + | + = note: this can happen when a constructor or expression leaves a type parameter unresolved + = note: help: add a type annotation that fixes the concrete type diff --git a/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/main.solc b/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/main.solc new file mode 100644 index 00000000..f64d88ee --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_integer_erasure_branch/main.solc @@ -0,0 +1,22 @@ +// integer value that survives to runtime because it is chosen by a runtime +// branch: the comptime evaluator cannot fold sload, so the integer inside +// Box cannot be erased. Judge cascade volume and span quality. +import * as std from std; + +enum Box { MkBox(integer) }; + +contract IntegerEscapesBranch { + function main() returns (word) { + let v: word; + assembly { + v := sload(0) + } + let b: Box = Box.MkBox(1); + if (v > 0) { + b = Box.MkBox(2); + } + match (b) { + case Box.MkBox(i) { return wordFromInteger(i); } + } + } +} diff --git a/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/diagnostics.snap new file mode 100644 index 00000000..9e286085 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/ergo_poly_entry/main.solc +--- +error[SC0401]: entry point must have a concrete, non-polymorphic type before specialization + --> /main/main.solc:5:1 + | +4 | +5 | / function main(x: a) returns (a) { +6 | | return x; +7 | | } + | |_^ type must be concrete here + | + = note: entry points are specialization roots and must have a single concrete type + = note: help: give the entry point a monomorphic signature or call a polymorphic helper from a monomorphic wrapper diff --git a/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/main.solc b/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/main.solc new file mode 100644 index 00000000..cc064337 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/ergo_poly_entry/main.solc @@ -0,0 +1,7 @@ +// Entry point whose type never becomes ground: `main` is polymorphic and is +// the specialization root (no contract), so ensure_closed fails with +// context "entry specialization". Judge the phrasing of that message. + +function main(x: a) returns (a) { + return x; +} diff --git a/crates/uitest/tests/fixtures/specialize/free_type_variable/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/free_type_variable/diagnostics.snap new file mode 100644 index 00000000..f0329eaf --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/free_type_variable/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/free_type_variable/main.solc +--- +error[SC0401]: cannot specialize expression: type is not concrete + --> /main/main.solc:8:13 + | +7 | function main() public returns (()) { +8 | let x = leak(); + | ^^^^^^ type must be concrete here +9 | return (); + | + = note: this can happen when a constructor or expression leaves a type parameter unresolved + = note: help: add a type annotation that fixes the concrete type diff --git a/crates/uitest/tests/fixtures/specialize/free_type_variable/main.solc b/crates/uitest/tests/fixtures/specialize/free_type_variable/main.solc new file mode 100644 index 00000000..fad862cf --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/free_type_variable/main.solc @@ -0,0 +1,11 @@ +function leak() returns (a) { + let y: a; + return y; +} + +contract C { + function main() public returns (()) { + let x = leak(); + return (); + } +} diff --git a/crates/uitest/tests/fixtures/specialize/integer_erasure/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/integer_erasure/diagnostics.snap new file mode 100644 index 00000000..809aa98f --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/integer_erasure/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/integer_erasure/main.solc +--- +error[SC0411]: runtime lowering cannot represent `integer` in return type of `main` + --> /main/main.solc:2:3 + | +1 | contract C { +2 | / function main() public returns (integer) { +3 | | return 1; +4 | | } + | |___^ not representable at runtime +5 | } + | + = note: `integer` and `comptime` values must be eliminated before runtime lowering + = note: help: evaluate the value at comptime or change it to a runtime-representable type diff --git a/crates/uitest/tests/fixtures/specialize/integer_erasure/main.solc b/crates/uitest/tests/fixtures/specialize/integer_erasure/main.solc new file mode 100644 index 00000000..74f1f2d9 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/integer_erasure/main.solc @@ -0,0 +1,5 @@ +contract C { + function main() public returns (integer) { + return 1; + } +} diff --git a/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/diagnostics.snap new file mode 100644 index 00000000..c42831d6 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/main.solc +--- +error[SC0414]: `id` cannot be reduced at compile time: recursive calls form a cycle with no base case (infinite recursion) + --> /main/main.solc:4:55 + | +3 | contract Answer { function main() returns (word) { return id(0); } +4 | function id(x: word) public returns (word) { return id(x); } + | ^^^^^ recursive call cannot be reduced here +5 | } + | + = note: help: add a base case, or guard the recursive call behind a runtime condition so it compiles to a runtime call + = note: a top-level function `id` is shadowed here; qualify the call to reach it diff --git a/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/main.solc b/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/main.solc new file mode 100644 index 00000000..41d54c43 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/non_comptime_unconditional_recursion/main.solc @@ -0,0 +1,5 @@ +function id(x: word) returns (word) { return x; } + +contract Answer { function main() returns (word) { return id(0); } + function id(x: word) public returns (word) { return id(x); } +} diff --git a/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/diagnostics.snap b/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/diagnostics.snap new file mode 100644 index 00000000..2122d7ad --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/main.solc +--- +error[SC0412]: specialization type size exceeded at 4096 type nodes + --> /main/main.solc:2:10 + | +1 | function go(x: a) returns (word) { +2 | return go((x, x)); + | ^^^^^^^^^^ specialization type size limit reached here +3 | } + | + = note: help: simplify the specialized type or raise --pe-max-type-nodes when using solcore-driver diff --git a/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/main.solc b/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/main.solc new file mode 100644 index 00000000..eb6e49b3 --- /dev/null +++ b/crates/uitest/tests/fixtures/specialize/polyrec_type_size_fuel/main.solc @@ -0,0 +1,9 @@ +function go(x: a) returns (word) { + return go((x, x)); +} + +contract C { + function main(x: word) public returns (word) { + return go(x); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/diagnostics.snap new file mode 100644 index 00000000..c9adb8cd --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/main.solc +--- +error[SC0229]: trait name used as type: `C` + --> /main/main.solc:4:10 + | +3 | function class_annotation() returns (word) { +4 | let x: C; + | ^ trait is not a type +5 | return 0; + | +--- + +error[SC0229]: trait name used as type: `Int` + --> /main/main.solc:9:10 + | + 8 | function builtin_class_annotation() returns (word) { + 9 | let x: Int = 1; + | ^^^ trait is not a type +10 | return x; + | diff --git a/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/main.solc b/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/main.solc new file mode 100644 index 00000000..bda4ce80 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_class_as_type_lowering/main.solc @@ -0,0 +1,11 @@ +trait C {} + +function class_annotation() returns (word) { + let x: C; + return 0; +} + +function builtin_class_annotation() returns (word) { + let x: Int = 1; + return x; +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/diagnostics.snap new file mode 100644 index 00000000..9d46c10b --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/diagnostics.snap @@ -0,0 +1,19 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/main.solc +--- +error[SC0203]: constructor expects 0 arguments, but 1 was provided + --> /main/main.solc:4:10 + | +1 | enum Opt { Some(word), None }; + | ---- `None` defined here +2 | +3 | function f() returns (Opt) { +4 | return Opt.None(1); + | ^^^^^^^^^^^ wrong number of arguments +5 | } + | + = note: expected 0 arguments + = note: found 1 argument + = note: `None` has signature `None() returns (Opt)` diff --git a/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/main.solc b/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/main.solc new file mode 100644 index 00000000..2597e8ea --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_ctor_arity_none/main.solc @@ -0,0 +1,5 @@ +enum Opt { Some(word), None }; + +function f() returns (Opt) { + return Opt.None(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/diagnostics.snap new file mode 100644 index 00000000..fa8ff813 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/diagnostics.snap @@ -0,0 +1,124 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/main.solc +--- +error[SC0201]: type mismatch: expected numeric, found Opt + --> /main/main.solc:5:10 + | +4 | function opt_ret() returns (Opt) { +5 | return 1; + | ^ expression has mismatched type +6 | } + | + = note: expected type: numeric + = note: found type: Opt +--- + +error[SC0201]: type mismatch: expected numeric, found bool + --> /main/main.solc:9:10 + | + 8 | function bool_ret() returns (bool) { + 9 | return 1; + | ^ expression has mismatched type +10 | } + | + = note: expected type: numeric + = note: found type: bool +--- + +error[SC0103]: undefined type constructor: string + --> /main/main.solc:12:32 + | +11 | +12 | function string_ret() returns (string) { + | ^^^^^^ undefined type constructor +13 | return 1; + | +--- + +error[SC0299]: ambiguous inferred type + --> /main/main.solc:12:40 + | +11 | +12 | function string_ret() returns (string) { + | ________________________________________^ +13 | | return 1; +14 | | } + | |_^ ambiguous inferred type +15 | + | + = note: <_> function() returns () where _: Int + = help: add a type annotation or a matching impl to fix the ambiguous type variable +--- + +error[SC0201]: type mismatch: expected numeric, found () + --> /main/main.solc:17:10 + | +16 | function unit_ret() returns (()) { +17 | return 1; + | ^ expression has mismatched type +18 | } + | + = note: expected type: numeric + = note: found type: () +--- + +error[SC0201]: type mismatch: expected numeric, found K + --> /main/main.solc:21:10 + | +20 | function contract_ret() returns (K) { +21 | return 1; + | ^ expression has mismatched type +22 | } + | + = note: expected type: numeric + = note: found type: K +--- + +error[SC0201]: type mismatch: expected numeric, found pair + --> /main/main.solc:25:10 + | +24 | function pair_ret() returns (pair) { +25 | return 1; + | ^ expression has mismatched type +26 | } + | + = note: expected type: numeric + = note: found type: pair +--- + +error[SC0201]: type mismatch: expected numeric, found sum + --> /main/main.solc:29:10 + | +28 | function sum_ret() returns (sum) { +29 | return 1; + | ^ expression has mismatched type +30 | } + | + = note: expected type: numeric + = note: found type: sum +--- + +error[SC0201]: type mismatch: expected numeric, found (word, word) + --> /main/main.solc:33:10 + | +32 | function tuple_ret() returns ((word, word)) { +33 | return 1; + | ^ expression has mismatched type +34 | } + | + = note: expected type: numeric + = note: found type: (word, word) +--- + +error[SC0201]: type mismatch: expected numeric, found function() returns (word) + --> /main/main.solc:37:10 + | +36 | function function_ret() returns (function() returns (word)) { +37 | return 1; + | ^ expression has mismatched type +38 | } + | + = note: expected type: numeric + = note: found type: function() returns (word) diff --git a/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/main.solc b/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/main.solc new file mode 100644 index 00000000..64eb64ec --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_literal_concrete_matrix/main.solc @@ -0,0 +1,38 @@ +enum Opt { Some(word), None }; +contract K { function main() returns (word) { return 0; } } + +function opt_ret() returns (Opt) { + return 1; +} + +function bool_ret() returns (bool) { + return 1; +} + +function string_ret() returns (string) { + return 1; +} + +function unit_ret() returns (()) { + return 1; +} + +function contract_ret() returns (K) { + return 1; +} + +function pair_ret() returns (pair) { + return 1; +} + +function sum_ret() returns (sum) { + return 1; +} + +function tuple_ret() returns ((word, word)) { + return 1; +} + +function function_ret() returns (function() returns (word)) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/diagnostics.snap new file mode 100644 index 00000000..ec1ac793 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/main.solc +--- +error[SC0201]: type mismatch: expected numeric, found Opt + --> /main/main.solc:4:10 + | +3 | function f() returns (Opt) { +4 | return 1; + | ^ expression has mismatched type +5 | } + | + = note: expected type: numeric + = note: found type: Opt diff --git a/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/main.solc b/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/main.solc new file mode 100644 index 00000000..cebe40ab --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_literal_vs_opt/main.solc @@ -0,0 +1,5 @@ +enum Opt { Some(word), None }; + +function f() returns (Opt) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/diagnostics.snap new file mode 100644 index 00000000..5d35f1d2 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/diagnostics.snap @@ -0,0 +1,50 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_obligation_classification/main.solc +--- +error[SC0201]: type mismatch: expected numeric, found function() returns (word) + --> /main/main.solc:2:10 + | +1 | function literal_as_callee() returns (word) { +2 | return 1(); + | ^ expression has mismatched type +3 | } + | + = note: expected type: numeric + = note: found type: function() returns (word) +--- + +error[SC0206]: non-callable value of type word + --> /main/main.solc:7:10 + | +6 | let x: word; +7 | return x(); + | ^ callee is not callable +8 | } + | +--- + +error[SC0201]: argument type mismatch in call to `fromInteger` + --> /main/main.solc:11:26 + | +10 | function from_integer_bad_arg() returns (word) { +11 | return Int.fromInteger(true); + | ^^^^ argument has mismatched type +12 | } + | + = note: expected `integer` because parameter 1 of `fromInteger` has type `integer` + = note: found type: bool + = note: `fromInteger` has signature `fromInteger(integer) returns (_)` +--- + +error[SC0207]: cannot satisfy trait constraint: a: invokable<(), word> + --> /main/main.solc:14:25 + | +13 | +14 | function open_invokable(x: a) returns (word) { + | ^ constraint originates here +15 | return invoke(x, ()); + | + = note: no visible impl matches `a: invokable<(), word>` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/main.solc b/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/main.solc new file mode 100644 index 00000000..e1282080 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_obligation_classification/main.solc @@ -0,0 +1,16 @@ +function literal_as_callee() returns (word) { + return 1(); +} + +function word_as_callee() returns (word) { + let x: word; + return x(); +} + +function from_integer_bad_arg() returns (word) { + return Int.fromInteger(true); +} + +function open_invokable(x: a) returns (word) { + return invoke(x, ()); +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_return_type_name/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_return_type_name/diagnostics.snap new file mode 100644 index 00000000..f38bedbc --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_return_type_name/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_return_type_name/main.solc +--- +error[SC0228]: type name used as value: `Opt` + --> /main/main.solc:4:10 + | +3 | function f() returns (Opt) { +4 | return Opt; + | ^^^ not a value +5 | } + | + = help: use a constructor or value binding here, not a namespace name diff --git a/crates/uitest/tests/fixtures/typeck/audit_return_type_name/main.solc b/crates/uitest/tests/fixtures/typeck/audit_return_type_name/main.solc new file mode 100644 index 00000000..5988cf5b --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_return_type_name/main.solc @@ -0,0 +1,5 @@ +enum Opt { Some(word), None }; + +function f() returns (Opt) { + return Opt; +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/diagnostics.snap new file mode 100644 index 00000000..c1333584 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/diagnostics.snap @@ -0,0 +1,136 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/main.solc +--- +error[SC0228]: type name used as value: `Opt` + --> /main/main.solc:9:10 + | + 8 | function adt_value() returns (word) { + 9 | return Opt; + | ^^^ not a value +10 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: type name used as value: `Alias` + --> /main/main.solc:13:10 + | +12 | function alias_value() returns (word) { +13 | return Alias; + | ^^^^^ not a value +14 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: type name used as value: `K` + --> /main/main.solc:17:10 + | +16 | function contract_value() returns (word) { +17 | return K; + | ^ not a value +18 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: trait name used as value: `C` + --> /main/main.solc:21:10 + | +20 | function class_value() returns (word) { +21 | return C; + | ^ not a value +22 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: type name used as value: `word` + --> /main/main.solc:25:10 + | +24 | function builtin_type_value() returns (word) { +25 | return word; + | ^^^^ not a value +26 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: trait name used as value: `Int` + --> /main/main.solc:29:10 + | +28 | function builtin_class_value() returns (word) { +29 | return Int; + | ^^^ not a value +30 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: type variable used as value: `a` + --> /main/main.solc:33:10 + | +32 | function type_var_value() returns (word) { +33 | return a; + | ^ not a value +34 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: module used as value: `U` + --> /main/main.solc:37:10 + | +36 | function module_value() returns (word) { +37 | return U; + | ^ not a value +38 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: type name used as callee: `Opt` + --> /main/main.solc:41:10 + | +40 | function type_as_callee() returns (word) { +41 | return Opt(); + | ^^^ not a value +42 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0228]: module used as callee: `U` + --> /main/main.solc:45:10 + | +44 | function module_as_callee() returns (word) { +45 | return U(); + | ^ not a value +46 | } + | + = help: use a constructor or value binding here, not a namespace name +--- + +error[SC0207]: cannot satisfy trait constraint: operator Add.add + --> /main/main.solc:49:10 + | +48 | function type_in_binop() returns (word) { +49 | return Opt + 1; + | ^^^^^^^ constraint originates here +50 | } + | + = note: no visible impl matches `operator Add.add` + = help: add a matching impl or strengthen the surrounding type context +--- + +error[SC0228]: type name used as value: `Opt` + --> /main/main.solc:49:10 + | +48 | function type_in_binop() returns (word) { +49 | return Opt + 1; + | ^^^ not a value +50 | } + | + = help: use a constructor or value binding here, not a namespace name diff --git a/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/main.solc b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/main.solc new file mode 100644 index 00000000..9b6bb55f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/main.solc @@ -0,0 +1,50 @@ +import * as U from util; + +enum Opt { Some(word), None }; +alias Alias = word; +contract K { function main() returns (word) { return 0; } } +trait C {} + +function adt_value() returns (word) { + return Opt; +} + +function alias_value() returns (word) { + return Alias; +} + +function contract_value() returns (word) { + return K; +} + +function class_value() returns (word) { + return C; +} + +function builtin_type_value() returns (word) { + return word; +} + +function builtin_class_value() returns (word) { + return Int; +} + +function type_var_value() returns (word) { + return a; +} + +function module_value() returns (word) { + return U; +} + +function type_as_callee() returns (word) { + return Opt(); +} + +function module_as_callee() returns (word) { + return U(); +} + +function type_in_binop() returns (word) { + return Opt + 1; +} diff --git a/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/util.solc b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/util.solc new file mode 100644 index 00000000..763b1212 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/audit_value_namespace_matrix/util.solc @@ -0,0 +1,3 @@ +function g() returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/diagnostics.snap new file mode 100644 index 00000000..e31fe295 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/call_arg_defined_here/main.solc +--- +error[SC0201]: argument type mismatch in call to `paint` + --> /main/main.solc:4:21 + | +3 | function go() returns (L.Color) { +4 | return L.paint(1, true); + | ^^^^ argument has mismatched type +5 | } + | + ::: /main/lib.solc:4 + | +4 | +5 | function paint(name: word, c: Color) returns (Color) { + | - parameter `c` defined here +6 | return c; + | + = note: expected `Color` because parameter `c` of `paint` has type `Color` + = note: found type: bool + = note: `paint` has signature `paint(name: word, c: Color) returns (Color)` diff --git a/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/lib.solc b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/lib.solc new file mode 100644 index 00000000..da5c4593 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/lib.solc @@ -0,0 +1,7 @@ +export { Color(*), paint }; + +enum Color { Red, Green }; + +function paint(name: word, c: Color) returns (Color) { + return c; +} diff --git a/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/main.solc b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/main.solc new file mode 100644 index 00000000..89ebf496 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arg_defined_here/main.solc @@ -0,0 +1,5 @@ +import * as L from lib; + +function go() returns (L.Color) { + return L.paint(1, true); +} diff --git a/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/diagnostics.snap new file mode 100644 index 00000000..b1bac50f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/diagnostics.snap @@ -0,0 +1,23 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/call_arity_defined_here/main.solc +--- +error[SC0203]: call expects 1 argument, but 0 were provided + --> /main/main.solc:4:10 + | +3 | function go() returns (word) { +4 | return L.id(); + | ^^^^^^ wrong number of arguments +5 | } + | + ::: /main/lib.solc:2 + | +2 | +3 | function id(x: word) returns (word) { + | -- `id` defined here +4 | return x; + | + = note: expected 1 argument + = note: found 0 arguments + = note: `id` has signature `id(x: word) returns (word)` diff --git a/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/lib.solc b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/lib.solc new file mode 100644 index 00000000..28a558b3 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/lib.solc @@ -0,0 +1,5 @@ +export { id }; + +function id(x: word) returns (word) { + return x; +} diff --git a/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/main.solc b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/main.solc new file mode 100644 index 00000000..208079b6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_arity_defined_here/main.solc @@ -0,0 +1,5 @@ +import * as L from lib; + +function go() returns (word) { + return L.id(); +} diff --git a/crates/uitest/tests/fixtures/typeck/call_wrong_arity/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/call_wrong_arity/diagnostics.snap new file mode 100644 index 00000000..4cadeaba --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_wrong_arity/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/call_wrong_arity/main.solc +--- +error[SC0203]: call expects 1 argument, but 0 were provided + --> /main/main.solc:6:10 + | +1 | function f(x: word) returns (word) { + | - `f` defined here +2 | return x; +3 | } +4 | +5 | function g() returns (word) { +6 | return f(); + | ^^^ wrong number of arguments +7 | } + | + = note: expected 1 argument + = note: found 0 arguments + = note: `f` has signature `f(x: word) returns (word)` diff --git a/crates/uitest/tests/fixtures/typeck/call_wrong_arity/main.solc b/crates/uitest/tests/fixtures/typeck/call_wrong_arity/main.solc new file mode 100644 index 00000000..a257e1c5 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/call_wrong_arity/main.solc @@ -0,0 +1,7 @@ +function f(x: word) returns (word) { + return x; +} + +function g() returns (word) { + return f(); +} diff --git a/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/diagnostics.snap new file mode 100644 index 00000000..3488d423 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/diagnostics.snap @@ -0,0 +1,35 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: Contract, Fallback>: RunContract + --> /main/main.solc:6:10 + | +5 | +6 | contract C { + | ^ constraint originates here +7 | function roundtrip(value: Point memory) public returns (word) { return 0; } + | + = note: no visible impl matches `Contract, Fallback>: RunContract` + = help: add a matching impl or strengthen the surrounding type context +--- + +error[SC0231]: ABI parameter cannot be represented in the ABI: adt:Point (only memory(string) and memory(bytes) have canonical ABI evidence) + --> /main/main.solc:7:3 + | +6 | contract C { +7 | function roundtrip(value: Point memory) public returns (word) { return 0; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +8 | } + | +--- + +error[SC0231]: roundtrip cannot be represented in the ABI: adt:Point (only memory(string) and memory(bytes) have canonical ABI evidence) + --> /main/main.solc:7:3 + | +6 | contract C { +7 | function roundtrip(value: Point memory) public returns (word) { return 0; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +8 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/main.solc b/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/main.solc new file mode 100644 index 00000000..0155ffc8 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/canonical_location_wrapper_user_adt_payload/main.solc @@ -0,0 +1,8 @@ +import std; +import std.dispatch; + +enum Point { Point(word, bool) }; + +contract C { + function roundtrip(value: Point memory) public returns (word) { return 0; } +} diff --git a/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/diagnostics.snap new file mode 100644 index 00000000..6f41e07b --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/main.solc +--- +error[SC0221]: invalid impl member signature for `f`: expected function(word) returns (word), got function(word) returns (bool) + --> /main/main.solc:6:3 + | +5 | impl C { +6 | function f(x: word) returns (bool) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ invalid impl method signature +7 | return true; + | + = note: the impl method must match the trait method after substituting the impl head diff --git a/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/main.solc b/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/main.solc new file mode 100644 index 00000000..8219bf3f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/comptime_class_head_method_signature/main.solc @@ -0,0 +1,9 @@ +trait C { + function f(x: a) returns (a); +} + +impl C { + function f(x: word) returns (bool) { + return true; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/diagnostics.snap new file mode 100644 index 00000000..a9e4c3c8 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/main.solc +--- +error[SC0240]: runtime value passed to comptime parameter 'x' of 'Wrap.unwrap' + --> /main/main.solc:25:20 + | +24 | function main() public returns (word) { +25 | return process(sloadWord()); + | ^^^^^^^^^^^ runtime value passed here +26 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/main.solc b/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/main.solc new file mode 100644 index 00000000..83b2d61e --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/comptime_class_method_runtime_arg/main.solc @@ -0,0 +1,27 @@ +trait Wrap { + function unwrap(comptime x: t) returns (comptime word); +} + +impl Wrap { + function unwrap(comptime x: word) returns (comptime word) { + return x; + } +} + +function process(z: t) returns (word) where t: Wrap { + return Wrap.unwrap(z); +} + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract C { + function main() public returns (word) { + return process(sloadWord()); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/diagnostics.snap new file mode 100644 index 00000000..1f836c12 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:2:13 + | +1 | contract C { +2 | x: word = true; + | ^^^^ expression has mismatched type +3 | function main() returns (()) { return (); } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/main.solc new file mode 100644 index 00000000..5fe6d4e2 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/contract_field_initializer_mismatch/main.solc @@ -0,0 +1,4 @@ +contract C { + x: word = true; + function main() returns (()) { return (); } +} diff --git a/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/diagnostics.snap new file mode 100644 index 00000000..53a31148 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/diagnostics.snap @@ -0,0 +1,51 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/desugar_origin_spans/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:2:23 + | +1 | contract C { function main() returns (word) { return 0; } +2 | seed: word = true ? false : 1; + | ^^^^^ expression has mismatched type +3 | } + | + = note: expected type: word + = note: found type: bool +--- + +error[SC0203]: tuple pattern expects 2 arguments, but 3 were provided + --> /main/main.solc:7:8 + | +6 | match (p) { +7 | case (a, b, c) { return a; } + | ^^^^^^^^^ wrong number of arguments +8 | } + | + = note: expected 2 arguments + = note: found 3 arguments +--- + +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:12:18 + | +11 | function if_source(b: bool) returns (word) { +12 | return b ? 1 : false; + | ^^^^^ expression has mismatched type +13 | } + | + = note: expected type: word + = note: found type: bool +--- + +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:16:10 + | +15 | function bool_source() returns (word) { +16 | return true; + | ^^^^ expression has mismatched type +17 | } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/main.solc b/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/main.solc new file mode 100644 index 00000000..fccdbcbd --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/desugar_origin_spans/main.solc @@ -0,0 +1,17 @@ +contract C { function main() returns (word) { return 0; } + seed: word = true ? false : 1; +} + +function pat_source(p: (word, word)) returns (word) { + match (p) { + case (a, b, c) { return a; } + } +} + +function if_source(b: bool) returns (word) { + return b ? 1 : false; +} + +function bool_source() returns (word) { + return true; +} diff --git a/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/diagnostics.snap new file mode 100644 index 00000000..c3c4bf27 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/diagnostics.snap @@ -0,0 +1,18 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/main.solc +--- +error[SC0229]: duplicate type definition: DispatchNameTy_C_ping + --> /main/main.solc:4:6 + | +3 | +4 | enum DispatchNameTy_C_ping { Collision }; + | ^^^^^^^^^^^^^^^^^^^^^ duplicate type +5 | +6 | contract C { +7 | function ping() public returns (uint256) { + | ---- existing definition +8 | return uint256.uint256(0); + | + = note: rename or remove the duplicate type definition diff --git a/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/main.solc b/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/main.solc new file mode 100644 index 00000000..d4d18faa --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/dispatch_name_collision_full/main.solc @@ -0,0 +1,10 @@ +import std; +import std.dispatch; + +enum DispatchNameTy_C_ping { Collision }; + +contract C { + function ping() public returns (uint256) { + return uint256.uint256(0); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/diagnostics.snap new file mode 100644 index 00000000..739ff830 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/main.solc +--- +warning[SC0303]: unreachable match arm + --> /main/main.solc:4:3 + | +3 | case 0 { return 0; } +4 | case 0 { return 1; } + | ^^^^^^^^^^^^^^^^^^^^ this arm is unreachable +5 | default { return 2; } + | + = note: this arm is covered by previous match arms diff --git a/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/main.solc b/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/main.solc new file mode 100644 index 00000000..c1e63385 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/duplicate_literal_unreachable/main.solc @@ -0,0 +1,7 @@ +function pick(x: word) returns (word) { + match (x) { + case 0 { return 0; } + case 0 { return 1; } + default { return 2; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/diagnostics.snap new file mode 100644 index 00000000..b47fd77a --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/main.solc +--- +warning[SC0303]: unreachable match arm + --> /main/main.solc:4:3 + | +3 | case 0x0A { return 0; } +4 | case 10 { return 1; } + | ^^^^^^^^^^^^^^^^^^^^^ this arm is unreachable +5 | default { return 2; } + | + = note: this arm is covered by previous match arms +--- + +warning[SC0303]: unreachable match arm + --> /main/main.solc:12:3 + | +11 | case 0 { return 0; } +12 | case 115792089237316195423570985008687907853269984665640564039457584007913129639936 { return 1; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this arm is unreachable +13 | default { return 2; } + | + = note: this arm is covered by previous match arms diff --git a/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/main.solc b/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/main.solc new file mode 100644 index 00000000..cf544fd0 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/equivalent_numeric_literal_unreachable/main.solc @@ -0,0 +1,23 @@ +function pick(x: word) returns (word) { + match (x) { + case 0x0A { return 0; } + case 10 { return 1; } + default { return 2; } + } +} + +function wrapped(x: word) returns (word) { + match (x) { + case 0 { return 0; } + case 115792089237316195423570985008687907853269984665640564039457584007913129639936 { return 1; } + default { return 2; } + } +} + +function exact(x: integer) returns (word) { + match (x) { + case 0 { return 0; } + case 115792089237316195423570985008687907853269984665640564039457584007913129639936 { return 1; } + default { return 2; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/diagnostics.snap new file mode 100644 index 00000000..ef78ffac --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/main.solc +--- +error[SC0201]: argument type mismatch in call to `paint` + --> /main/main.solc:8:19 + | +2 | +3 | function paint(name: word, c: Color) returns (Color) { + | - parameter `c` defined here +4 | return c; +... +7 | function go() returns (Color) { +8 | return paint(1, true); + | ^^^^ argument has mismatched type +9 | } + | + = note: expected `Color` because parameter `c` of `paint` has type `Color` + = note: found type: bool + = note: `paint` has signature `paint(name: word, c: Color) returns (Color)` diff --git a/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/main.solc new file mode 100644 index 00000000..ad062425 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_arg_type_mismatch/main.solc @@ -0,0 +1,9 @@ +enum Color { Red, Green }; + +function paint(name: word, c: Color) returns (Color) { + return c; +} + +function go() returns (Color) { + return paint(1, true); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/diagnostics.snap new file mode 100644 index 00000000..df8b91be --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:3:7 + | +2 | let x: word = 1; +3 | x = true; + | ^^^^ expression has mismatched type +4 | return x; + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/main.solc new file mode 100644 index 00000000..0832ee89 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_assign_mismatch/main.solc @@ -0,0 +1,5 @@ +function f() returns (word) { + let x: word = 1; + x = true; + return x; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/diagnostics.snap new file mode 100644 index 00000000..04719c1d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/main.solc +--- +error[SC0203]: call expects 3 arguments, but 1 was provided + --> /main/main.solc:6:10 + | +1 | function clamp(lo: word, hi: word, v: word) returns (word) { + | ----- `clamp` defined here +2 | return v; +3 | } +4 | +5 | function g() returns (word) { +6 | return clamp(1); + | ^^^^^^^^ wrong number of arguments +7 | } + | + = note: expected 3 arguments + = note: found 1 argument + = note: `clamp` has signature `clamp(lo: word, hi: word, v: word) returns (word)` diff --git a/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/main.solc new file mode 100644 index 00000000..d26bc65d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_call_too_few_args/main.solc @@ -0,0 +1,7 @@ +function clamp(lo: word, hi: word, v: word) returns (word) { + return v; +} + +function g() returns (word) { + return clamp(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/diagnostics.snap new file mode 100644 index 00000000..d3be6985 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/main.solc +--- +error[SC0203]: call expects 1 argument, but 3 were provided + --> /main/main.solc:6:10 + | +1 | function double(x: word) returns (word) { + | ------ `double` defined here +2 | return x; +3 | } +4 | +5 | function g() returns (word) { +6 | return double(1, 2, 3); + | ^^^^^^^^^^^^^^^ wrong number of arguments +7 | } + | + = note: expected 1 argument + = note: found 3 arguments + = note: `double` has signature `double(x: word) returns (word)` diff --git a/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/main.solc new file mode 100644 index 00000000..f4933dc5 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_call_too_many_args/main.solc @@ -0,0 +1,7 @@ +function double(x: word) returns (word) { + return x; +} + +function g() returns (word) { + return double(1, 2, 3); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/diagnostics.snap new file mode 100644 index 00000000..4ca7d1b6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/main.solc +--- +error[SC0103]: undefined type constructor: a + --> /main/main.solc:2:19 + | +1 | trait C { +2 | function use(x: a) returns (b) ; + | ^ undefined type constructor +3 | } + | + = help: did you mean type `C`? diff --git a/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/main.solc new file mode 100644 index 00000000..9581355c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_class_head_no_forall/main.solc @@ -0,0 +1,3 @@ +trait C { + function use(x: a) returns (b) ; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/diagnostics.snap new file mode 100644 index 00000000..14b99c60 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: operator Add.add + --> /main/main.solc:17:12 + | +16 | function double(comptime x: word) returns (comptime word) { +17 | return x + x; + | ^^^^^ constraint originates here +18 | } + | + = note: no visible impl matches `operator Add.add` + = help: add a matching impl or strengthen the surrounding type context +--- + +error[SC0240]: runtime value passed to comptime parameter 'x' of 'double' + --> /main/main.solc:20:43 + | +19 | function main() returns (word) { +20 | let g = lam (y: word) { return double(y); }; + | ^ runtime value passed here +21 | return g(sloadWord()); + | diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/main.solc new file mode 100644 index 00000000..9e0d6436 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ct_indirect_escape/main.solc @@ -0,0 +1,23 @@ +// Smuggle a runtime value into a comptime parameter through a function +// value: bind the comptime function to a local, then call the local with +// a runtime argument. If the SAIL comptime check only looks at direct +// calls, this silently defeats the comptime contract (accept-bug). +import * as std from std; + +function sloadWord() returns (word) { + let v: word; + assembly { + v := sload(0) + } + return v; +} + +contract CtIndirectEscape { + function double(comptime x: word) returns (comptime word) { + return x + x; + } + function main() returns (word) { + let g = lam (y: word) { return double(y); }; + return g(sloadWord()); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/diagnostics.snap new file mode 100644 index 00000000..581721a6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/diagnostics.snap @@ -0,0 +1,19 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/main.solc +--- +error[SC0203]: constructor expects 2 arguments, but 1 was provided + --> /main/main.solc:4:10 + | +1 | enum Pair { Mk(a, b) }; + | -- `Mk` defined here +2 | +3 | function f() returns (Pair) { +4 | return Pair.Mk(1); + | ^^^^^^^^^^ wrong number of arguments +5 | } + | + = note: expected 2 arguments + = note: found 1 argument + = note: `Mk` has signature `Mk(a, b) returns (Pair)` diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/main.solc new file mode 100644 index 00000000..b4907c82 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_expr/main.solc @@ -0,0 +1,5 @@ +enum Pair { Mk(a, b) }; + +function f() returns (Pair) { + return Pair.Mk(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/diagnostics.snap new file mode 100644 index 00000000..646e1944 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/main.solc +--- +error[SC0203]: constructor pattern expects 2 arguments, but 1 was provided + --> /main/main.solc:5:8 + | +4 | match (p) { +5 | case Pair.Mk(x) { return x; } + | ^^^^^^^^^^ wrong number of arguments +6 | } + | + = note: expected 2 arguments + = note: found 1 argument diff --git a/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/main.solc new file mode 100644 index 00000000..a2816ddc --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_ctor_arity_pattern/main.solc @@ -0,0 +1,7 @@ +enum Pair { Mk(a, b) }; + +function f(p: Pair) returns (word) { + match (p) { + case Pair.Mk(x) { return x; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/diagnostics.snap new file mode 100644 index 00000000..900307ff --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/diagnostics.snap @@ -0,0 +1,21 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/main.solc +--- +error[SC0201]: argument type mismatch in call to `add3` + --> /main/main.solc:7:34 + | +1 | function add3(a: word, b: word, c: word) returns (word) { + | - parameter `b` defined here +2 | return a; +3 | } +... +6 | return add3(add3(x, x, add3(x, add3(x, x, x), x)), +7 | add3(x, x, add3(x, true, x)), + | ^^^^ argument has mismatched type +8 | x); + | + = note: expected `word` because parameter `b` of `add3` has type `word` + = note: found type: bool + = note: `add3` has signature `add3(a: word, b: word, c: word) returns (word)` diff --git a/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/main.solc new file mode 100644 index 00000000..0e503f28 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_deep_nested_mismatch/main.solc @@ -0,0 +1,9 @@ +function add3(a: word, b: word, c: word) returns (word) { + return a; +} + +function f(x: word) returns (word) { + return add3(add3(x, x, add3(x, add3(x, x, x), x)), + add3(x, x, add3(x, true, x)), + x); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/diagnostics.snap new file mode 100644 index 00000000..2a8d6baf --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/main.solc +--- +error[SC0205]: cannot resolve field `red` + --> /main/main.solc:4:12 + | +3 | function f(c: Color) returns (word) { +4 | return c.red; + | ^^^ unknown field +5 | } + | + = help: check that the receiver has this field or constructor path diff --git a/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/main.solc new file mode 100644 index 00000000..fc4eec85 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_field_access_non_struct/main.solc @@ -0,0 +1,5 @@ +enum Color { Red, Green }; + +function f(c: Color) returns (word) { + return c.red; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/diagnostics.snap new file mode 100644 index 00000000..6052cc2f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: a: Int + --> /main/main.solc:1:16 + | +1 | function ident(x: a) returns (a) { + | ^ constraint originates here +2 | return 1; +3 | } + | + = note: no visible impl matches `a: Int` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/main.solc new file mode 100644 index 00000000..0e598ee8 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_forall_tyvar_mismatch/main.solc @@ -0,0 +1,3 @@ +function ident(x: a) returns (a) { + return 1; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/diagnostics.snap new file mode 100644 index 00000000..7ab227ac --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/main.solc +--- +error[SC0203]: Yul call `dbl` expects 1 argument, but 2 were provided + --> /main/main.solc:8:12 + | +7 | } +8 | x := dbl(1, 2) + | ^^^^^^^^^ wrong number of arguments +9 | } + | + = note: expected 1 argument + = note: found 2 arguments diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/main.solc new file mode 100644 index 00000000..cb9389ff --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_call_arity/main.solc @@ -0,0 +1,12 @@ +contract C { + function main() public returns (word) { + let x: word; + assembly { + function dbl(a) -> r { + r := add(a, a) + } + x := dbl(1, 2) + } + return x; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/diagnostics.snap new file mode 100644 index 00000000..0caaef49 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/main.solc +--- +error[SC0211]: unknown Yul identifier or function: someUndefinedThing + --> /main/main.solc:5:12 + | +4 | assembly { +5 | x := someUndefinedThing + | ^^^^^^^^^^^^^^^^^^ unknown Yul name +6 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/main.solc new file mode 100644 index 00000000..a4450d57 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_asm_undefined_var/main.solc @@ -0,0 +1,9 @@ +contract C { + function main() public returns (word) { + let x: word; + assembly { + x := someUndefinedThing + } + return x; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/diagnostics.snap new file mode 100644 index 00000000..75c84588 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/main.solc +--- +error[SC0203]: match arm expects 2 arguments, but 1 was provided + --> /main/main.solc:5:3 + | +4 | match (x, y) { +5 | case Nat.Zero { return 0; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ wrong number of arguments +6 | case (Nat.Succ(a), Nat.Zero) { return 1; } + | + = note: expected 2 arguments + = note: found 1 argument diff --git a/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/main.solc new file mode 100644 index 00000000..d3558f0e --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_hull_match_arm_arity/main.solc @@ -0,0 +1,15 @@ +enum Nat { Zero, Succ(Nat) }; + +function pick(x: Nat, y: Nat) returns (word) { + match (x, y) { + case Nat.Zero { return 0; } + case (Nat.Succ(a), Nat.Zero) { return 1; } + case (Nat.Succ(a), Nat.Succ(b)) { return 2; } + } +} + +contract T { + function main() public returns (word) { + return pick(Nat.Zero, Nat.Zero); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/diagnostics.snap new file mode 100644 index 00000000..7cd89b07 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected numeric, found bool + --> /main/main.solc:2:19 + | +1 | function f(b: bool) returns (word) { +2 | let x = b ? 1 : false; + | ^^^^^ expression has mismatched type +3 | return x; + | + = note: expected type: numeric + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/main.solc new file mode 100644 index 00000000..6cdd5099 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_if_expr_branch_mismatch/main.solc @@ -0,0 +1,4 @@ +function f(b: bool) returns (word) { + let x = b ? 1 : false; + return x; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/diagnostics.snap new file mode 100644 index 00000000..412dc3cb --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/main.solc +--- +error[SC0220]: top-level function must have complete type annotations + --> /main/main.solc:2:12 + | +1 | contract C { +2 | function id(x) public { + | ^^ incomplete signature +3 | return x; + | + = note: signature: function id(x) public + = note: annotate every parameter (`name: Type`); omit `returns` for a function that returns no value diff --git a/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/main.solc new file mode 100644 index 00000000..1ef672a2 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_incomplete_sig_accepted/main.solc @@ -0,0 +1,9 @@ +contract C { + function id(x) public { + return x; + } + + function main() returns (word) { + return 0; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/diagnostics.snap new file mode 100644 index 00000000..971a8b83 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:6:39 + | +5 | function g() returns (word) { +6 | return apply(lam (y: word) { return true; }, 1); + | ^^^^ expression has mismatched type +7 | } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/main.solc new file mode 100644 index 00000000..54943cdf --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_lambda_body_mismatch/main.solc @@ -0,0 +1,7 @@ +function apply(f: function(word) returns (word), x: word) returns (word) { + return f(x); +} + +function g() returns (word) { + return apply(lam (y: word) { return true; }, 1); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/diagnostics.snap new file mode 100644 index 00000000..2da67be1 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:6:33 + | +5 | case Shape.Circle(r) { return r; } +6 | case Shape.Square(w) { return true; } + | ^^^^ expression has mismatched type +7 | } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/main.solc new file mode 100644 index 00000000..45a0c60f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_match_branch_divergence/main.solc @@ -0,0 +1,8 @@ +enum Shape { Circle(word), Square(word) }; + +function area(s: Shape) returns (word) { + match (s) { + case Shape.Circle(r) { return r; } + case Shape.Square(w) { return true; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/diagnostics.snap new file mode 100644 index 00000000..916d1cdb --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/diagnostics.snap @@ -0,0 +1,37 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:2:10 + | +1 | function a() returns (word) { +2 | return true; + | ^^^^ expression has mismatched type +3 | } + | + = note: expected type: word + = note: found type: bool +--- + +error[SC0201]: type mismatch: expected numeric, found bool + --> /main/main.solc:6:10 + | +5 | function b() returns (bool) { +6 | return 1; + | ^ expression has mismatched type +7 | } + | + = note: expected type: numeric + = note: found type: bool +--- + +error[SC0206]: non-callable value of type word + --> /main/main.solc:10:10 + | + 9 | function c(x: word) returns (word) { +10 | return x(1); + | ^ callee is not callable +11 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/main.solc new file mode 100644 index 00000000..9d2e8642 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_multi_independent_errors/main.solc @@ -0,0 +1,11 @@ +function a() returns (word) { + return true; +} + +function b() returns (bool) { + return 1; +} + +function c(x: word) returns (word) { + return x(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/diagnostics.snap new file mode 100644 index 00000000..9e9a3e0c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/main.solc +--- +error[SC0202]: recursive type would be required + --> /main/main.solc:4:12 + | +3 | let g = x(y); +4 | return g(x); + | ^^^^ recursive type required here +5 | }; + | + = note: an inferred type would need to contain itself + = note: recursive shape: function(function(_) returns (_)) returns (_) + = help: add an explicit type annotation or split the recursive call diff --git a/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/main.solc new file mode 100644 index 00000000..254cff29 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_occurs_lambda_msg/main.solc @@ -0,0 +1,7 @@ +function f() returns (()) { + let s = lam (x, y) { + let g = x(y); + return g(x); + }; + return (); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/diagnostics.snap new file mode 100644 index 00000000..e81a8869 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/main.solc +--- +error[SC0201]: type mismatch: expected Shape, found Color + --> /main/main.solc:6:8 + | +5 | match (c) { +6 | case Shape.Circle(r) { return r; } + | ^^^^^^^^^^^^^^^ expression has mismatched type +7 | } + | + = note: expected type: Shape + = note: found type: Color diff --git a/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/main.solc new file mode 100644 index 00000000..ac4c3a94 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_pattern_wrong_type/main.solc @@ -0,0 +1,8 @@ +enum Color { Red, Green }; +enum Shape { Circle(word) }; + +function f(c: Color) returns (word) { + match (c) { + case Shape.Circle(r) { return r; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/diagnostics.snap new file mode 100644 index 00000000..c91bf4e6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/diagnostics.snap @@ -0,0 +1,33 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/main.solc +--- +error[SC0201]: argument type mismatch in call to `first` + --> /main/main.solc:8:17 + | +1 | function first(p: (word, word)) returns (word) { + | - parameter `p` defined here +2 | match (p) { +3 | case (a, b) { return a; } +... +7 | function f() returns (word) { +8 | let x = first(true); + | ^^^^ argument has mismatched type +9 | return x; + | + = note: expected `(word, word)` because parameter `p` of `first` has type `(word, word)` + = note: found type: bool + = note: `first` has signature `first(p: (word, word)) returns (word)` +--- + +error[SC0201]: type mismatch: expected numeric, found bool + --> /main/main.solc:13:10 + | +12 | function g() returns (bool) { +13 | return 42; + | ^^ expression has mismatched type +14 | } + | + = note: expected type: numeric + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/main.solc new file mode 100644 index 00000000..d3747564 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_recovery_no_cascade/main.solc @@ -0,0 +1,14 @@ +function first(p: (word, word)) returns (word) { + match (p) { + case (a, b) { return a; } + } +} + +function f() returns (word) { + let x = first(true); + return x; +} + +function g() returns (bool) { + return 42; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/diagnostics.snap new file mode 100644 index 00000000..71b635eb --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/main.solc +--- +error[SC0201]: type mismatch: expected word, found Color + --> /main/main.solc:4:10 + | +3 | function pick() returns (word) { +4 | return Color.Red; + | ^^^^^^^^^ expression has mismatched type +5 | } + | + = note: expected type: word + = note: found type: Color diff --git a/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/main.solc new file mode 100644 index 00000000..9f38710f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_return_type_mismatch_data/main.solc @@ -0,0 +1,5 @@ +enum Color { Red, Green }; + +function pick() returns (word) { + return Color.Red; +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/diagnostics.snap new file mode 100644 index 00000000..d5e3d9b6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/main.solc +--- +error[SC0203]: tuple expects 3 arguments, but 2 were provided + --> /main/main.solc:2:10 + | +1 | function f() returns ((word, word, word)) { +2 | return (1, 2); + | ^^^^^^ wrong number of arguments +3 | } + | + = note: expected 3 arguments + = note: found 2 arguments diff --git a/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/main.solc new file mode 100644 index 00000000..337be93c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_tuple_arity_mismatch/main.solc @@ -0,0 +1,3 @@ +function f() returns ((word, word, word)) { + return (1, 2); +} diff --git a/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/diagnostics.snap new file mode 100644 index 00000000..f9819673 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ergo_type_as_value/main.solc +--- +error[SC0228]: type name used as value: `Pair` + --> /main/main.solc:4:11 + | +3 | function main() returns (word) { +4 | let p = Pair; + | ^^^^ not a value +5 | return 0; + | + = help: use a constructor or value binding here, not a namespace name diff --git a/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/main.solc b/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/main.solc new file mode 100644 index 00000000..13c80f74 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ergo_type_as_value/main.solc @@ -0,0 +1,6 @@ +enum Pair { MkPair(word, word) }; + +function main() returns (word) { + let p = Pair; + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/diagnostics.snap new file mode 100644 index 00000000..8a2aefb9 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found () + --> /main/main.solc:2:3 + | +1 | function f(x: bool) returns (word) { +2 | if (x) { 1; } else { true; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expression has mismatched type +3 | } + | + = note: expected type: word + = note: found type: () diff --git a/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/main.solc new file mode 100644 index 00000000..a67bed2f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/final_if_branch_mismatch/main.solc @@ -0,0 +1,3 @@ +function f(x: bool) returns (word) { + if (x) { 1; } else { true; } +} diff --git a/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/diagnostics.snap new file mode 100644 index 00000000..551a64ce --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/diagnostics.snap @@ -0,0 +1,174 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/main.solc +--- +error[SC0101]: undefined name: Contract + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0101]: undefined name: Fallback + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0101]: undefined name: Method + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | + = help: did you mean `echo`? +--- + +error[SC0101]: undefined name: Proxy + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0101]: undefined name: RunContract + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0101]: undefined name: fallback_default_implementation + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ unknown name +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0103]: undefined type constructor: NonPayable + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ undefined type constructor +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0103]: undefined type constructor: Proxy + --> /main/main.solc:1:10 + | +1 | contract C { + | ^ undefined type constructor +2 | function echo(value: uint256) public returns (uint256) { return value; } +3 | } + | +--- + +error[SC0103]: undefined type constructor: NonPayable + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ undefined type constructor +3 | } + | +--- + +error[SC0103]: undefined type constructor: Proxy + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ undefined type constructor +3 | } + | +--- + +error[SC0103]: undefined type constructor: string + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ undefined type constructor +3 | } + | +--- + +error[SC0105]: undefined trait: SigString + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ undefined trait +3 | } + | +--- + +error[SC0231]: ABI output cannot be represented in the ABI: + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +3 | } + | +--- + +error[SC0231]: ABI parameter cannot be represented in the ABI: + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +3 | } + | +--- + +error[SC0231]: echo cannot be represented in the ABI: + --> /main/main.solc:2:3 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +3 | } + | +--- + +error[SC0103]: undefined type constructor: uint256 + --> /main/main.solc:2:24 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^ undefined type constructor +3 | } + | +--- + +error[SC0103]: undefined type constructor: uint256 + --> /main/main.solc:2:49 + | +1 | contract C { +2 | function echo(value: uint256) public returns (uint256) { return value; } + | ^^^^^^^ undefined type constructor +3 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/main.solc b/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/main.solc new file mode 100644 index 00000000..6b78753c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/generated_dispatch_requires_explicit_imports/main.solc @@ -0,0 +1,3 @@ +contract C { + function echo(value: uint256) public returns (uint256) { return value; } +} diff --git a/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/diagnostics.snap new file mode 100644 index 00000000..22fc2d7d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/inferred_poly_compose/main.solc +--- +error[SC0220]: top-level function must have complete type annotations + --> /main/main.solc:2:12 + | +1 | contract C { +2 | function compose(f, g) public { + | ^^^^^^^ incomplete signature +3 | return lam (x) { + | + = note: signature: function compose(f, g) public + = note: annotate every parameter (`name: Type`); omit `returns` for a function that returns no value diff --git a/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/main.solc b/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/main.solc new file mode 100644 index 00000000..ef0c6756 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/inferred_poly_compose/main.solc @@ -0,0 +1,16 @@ +contract C { + function compose(f, g) public { + return lam (x) { + return f(g(x)); + }; + } + + function id(x: word) public returns (word) { + return x; + } + + function main() public returns (word) { + let f = compose(id, id); + return f(42); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/diagnostics.snap new file mode 100644 index 00000000..98c3510c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/diagnostics.snap @@ -0,0 +1,17 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/let_unannotated_literal/main.solc +--- +error[SC0299]: ambiguous inferred type + --> /main/main.solc:1:29 + | +1 | function f() returns (word) { + | _____________________________^ +2 | | let y = 7; +3 | | return 0; +4 | | } + | |_^ ambiguous inferred type + | + = note: <_> function() returns (word) where _: Int + = help: add a type annotation or a matching impl to fix the ambiguous type variable diff --git a/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/main.solc b/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/main.solc new file mode 100644 index 00000000..6b91a3a2 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/let_unannotated_literal/main.solc @@ -0,0 +1,4 @@ +function f() returns (word) { + let y = 7; + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/diagnostics.snap new file mode 100644 index 00000000..b1084703 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/diagnostics.snap @@ -0,0 +1,45 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: Contract, Fallback>: RunContract + --> /main/main.solc:7:10 + | +6 | +7 | contract Shapes { + | ^^^^^^ constraint originates here +8 | function roundtrip(p: Point) public returns (Point) { return p; } + | + = note: no visible impl matches `Contract, Fallback>: RunContract` + = help: add a matching impl or strengthen the surrounding type context +--- + +error[SC0231]: ABI output cannot be represented in the ABI: Point (user-defined ADTs are not supported by the canonical external ABI) + --> /main/main.solc:8:3 + | +7 | contract Shapes { +8 | function roundtrip(p: Point) public returns (Point) { return p; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +9 | } + | +--- + +error[SC0231]: ABI parameter cannot be represented in the ABI: Point (user-defined ADTs are not supported by the canonical external ABI) + --> /main/main.solc:8:3 + | +7 | contract Shapes { +8 | function roundtrip(p: Point) public returns (Point) { return p; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +9 | } + | +--- + +error[SC0231]: roundtrip cannot be represented in the ABI: Point (user-defined ADTs are not supported by the canonical external ABI) + --> /main/main.solc:8:3 + | +7 | contract Shapes { +8 | function roundtrip(p: Point) public returns (Point) { return p; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +9 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/main.solc b/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/main.solc new file mode 100644 index 00000000..afc94589 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/manual_generic_adt_external_abi/main.solc @@ -0,0 +1,9 @@ +import std; +import std.dispatch; + +pragma solcore noGenericInstanceFor Point; +enum Point { Point(word, word) }; + +contract Shapes { + function roundtrip(p: Point) public returns (Point) { return p; } +} diff --git a/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/diagnostics.snap new file mode 100644 index 00000000..b4169d11 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/match_branch_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:4:23 + | +3 | case true { return 1; } +4 | case false { return true; } + | ^^^^ expression has mismatched type +5 | } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/main.solc new file mode 100644 index 00000000..cab05e33 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/match_branch_mismatch/main.solc @@ -0,0 +1,6 @@ +function h(x: bool) returns (word) { + match (x) { + case true { return 1; } + case false { return true; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/diagnostics.snap new file mode 100644 index 00000000..6d63ec91 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: Contract, Fallback>: RunContract + --> /main/main.solc:7:10 + | +6 | // with a bounded solver diagnostic while that evidence is missing. +7 | contract WordAbiProbe { + | ^^^^^^^^^^^^ constraint originates here +8 | function echo(value: word) public returns (word) { + | + = note: no visible impl matches `Contract, Fallback>: RunContract` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/main.solc b/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/main.solc new file mode 100644 index 00000000..6dd33687 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/missing_word_abi_evidence/main.solc @@ -0,0 +1,11 @@ +import std; +import std.dispatch; + +// `word` has ABI metadata (`uint256`) but the pinned shared std does not yet +// provide its selector/decode/encode evidence. The frontend must terminate +// with a bounded solver diagnostic while that evidence is missing. +contract WordAbiProbe { + function echo(value: word) public returns (word) { + return value; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/diagnostics.snap new file mode 100644 index 00000000..f83b43ff --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/mutual_recursive_data/main.solc +--- +error[SC0203]: undefined type: A + --> /main/main.solc:2:12 + | +1 | enum A { A(B) }; +2 | enum B { B(A) }; + | ^ undefined type +3 | + | diff --git a/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/main.solc b/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/main.solc new file mode 100644 index 00000000..5a8d12de --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/mutual_recursive_data/main.solc @@ -0,0 +1,6 @@ +enum A { A(B) }; +enum B { B(A) }; + +function f(x: A) returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/diagnostics.snap new file mode 100644 index 00000000..4f183c98 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:5:10 + | +4 | function pick(x: Outer) returns (word) { +5 | match (x) { + | ^ non-exhaustive match +6 | case Outer.Other { return 0; } + | + = note: missing case: Outer.Wrap(Inner.B) + = note: help: add a clause that covers the missing case diff --git a/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/main.solc b/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/main.solc new file mode 100644 index 00000000..fc27df9d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nested_constructor_nonexhaustive/main.solc @@ -0,0 +1,9 @@ +enum Inner { A, B }; +enum Outer { Other, Wrap(Inner) }; + +function pick(x: Outer) returns (word) { + match (x) { + case Outer.Other { return 0; } + case Outer.Wrap(Inner.A) { return 1; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/diagnostics.snap new file mode 100644 index 00000000..a0d5d783 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/main.solc +--- +warning[SC0303]: unreachable match arm + --> /main/main.solc:7:3 + | +6 | case Outer.Wrap(_) { return 0; } +7 | case Outer.Wrap(Inner.A) { return 1; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this arm is unreachable +8 | case Outer.Other { return 2; } + | + = note: this arm is covered by previous match arms diff --git a/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/main.solc b/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/main.solc new file mode 100644 index 00000000..1ffa5d7e --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nested_constructor_unreachable/main.solc @@ -0,0 +1,10 @@ +enum Inner { A, B }; +enum Outer { Other, Wrap(Inner) }; + +function pick(x: Outer) returns (word) { + match (x) { + case Outer.Wrap(_) { return 0; } + case Outer.Wrap(Inner.A) { return 1; } + case Outer.Other { return 2; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/diagnostics.snap new file mode 100644 index 00000000..10bc5c5d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:5:12 + | +4 | function pick(x: Flag) public returns (word) { +5 | match (x) { + | ^ non-exhaustive match +6 | case Flag.Off { return 0; } + | + = note: missing case: Flag.On + = note: help: add a clause that covers the missing case diff --git a/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/main.solc b/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/main.solc new file mode 100644 index 00000000..16f1f28d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonexhaustive_contract/main.solc @@ -0,0 +1,13 @@ +contract C { + enum Flag { Off, On }; + + function pick(x: Flag) public returns (word) { + match (x) { + case Flag.Off { return 0; } + } + } + + function main() returns (word) { + return 0; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/diagnostics.snap new file mode 100644 index 00000000..4c0873a9 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:4:10 + | +3 | function pick(x: Flag) returns (word) { +4 | match (x) { + | ^ non-exhaustive match +5 | case Flag.Off { return 0; } + | + = note: missing case: Flag.On + = note: help: add a clause that covers the missing case diff --git a/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/main.solc b/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/main.solc new file mode 100644 index 00000000..3747b15a --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonexhaustive_free_fn/main.solc @@ -0,0 +1,7 @@ +enum Flag { Off, On }; + +function pick(x: Flag) returns (word) { + match (x) { + case Flag.Off { return 0; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/nonfinal_return/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nonfinal_return/diagnostics.snap new file mode 100644 index 00000000..4f423f92 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonfinal_return/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nonfinal_return/main.solc +--- +error[SC0222]: illegal return statement + --> /main/main.solc:2:3 + | +1 | function g() returns (word) { +2 | return 1; + | ^^^^^^^^^ return before end of block +3 | return 2; + | + = note: return statements must be the final statement in a block diff --git a/crates/uitest/tests/fixtures/typeck/nonfinal_return/main.solc b/crates/uitest/tests/fixtures/typeck/nonfinal_return/main.solc new file mode 100644 index 00000000..3ef4fe10 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nonfinal_return/main.solc @@ -0,0 +1,4 @@ +function g() returns (word) { + return 1; + return 2; +} diff --git a/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/diagnostics.snap new file mode 100644 index 00000000..e0b1aa02 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/main.solc +--- +error[SC0299]: Invalid number of type arguments! + --> /main/main.solc:4:10 + | +3 | function f() returns (word) { +4 | let x: M = M.Mk; + | ^^^^^^^ diagnostic reported here +5 | return 0; + | + = note: Type M is expected to have 0 type arguments + = note: but, type M has 1 arguments diff --git a/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/main.solc b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/main.solc new file mode 100644 index 00000000..118d83a5 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_let/main.solc @@ -0,0 +1,6 @@ +enum M { Mk }; + +function f() returns (word) { + let x: M = M.Mk; + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/diagnostics.snap new file mode 100644 index 00000000..cc0b6e42 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/main.solc +--- +error[SC0299]: Invalid number of type arguments! + --> /main/main.solc:3:15 + | +2 | +3 | function f(x: M) returns (word) { + | ^^^^^^^ diagnostic reported here +4 | return 0; + | + = note: Type M is expected to have 0 type arguments + = note: but, type M has 1 arguments diff --git a/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/main.solc b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/main.solc new file mode 100644 index 00000000..27728b98 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/nullary_type_applied_signature/main.solc @@ -0,0 +1,5 @@ +enum M { Mk }; + +function f(x: M) returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/occurs_check/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/occurs_check/diagnostics.snap new file mode 100644 index 00000000..d17c868a --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/occurs_check/diagnostics.snap @@ -0,0 +1,16 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/occurs_check/main.solc +--- +error[SC0202]: recursive type would be required + --> /main/main.solc:2:31 + | +1 | function f() returns (()) { +2 | let self = lam (x) { return x(x); }; + | ^^^^ recursive type required here +3 | return (); + | + = note: an inferred type would need to contain itself + = note: recursive shape: function(_) returns (_) + = help: add an explicit type annotation or split the recursive call diff --git a/crates/uitest/tests/fixtures/typeck/occurs_check/main.solc b/crates/uitest/tests/fixtures/typeck/occurs_check/main.solc new file mode 100644 index 00000000..ba7896a0 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/occurs_check/main.solc @@ -0,0 +1,4 @@ +function f() returns (()) { + let self = lam (x) { return x(x); }; + return (); +} diff --git a/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/diagnostics.snap new file mode 100644 index 00000000..313741b0 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/diagnostics.snap @@ -0,0 +1,6 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/main.solc +--- +no diagnostics diff --git a/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/main.solc b/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/main.solc new file mode 100644 index 00000000..5c5c784b --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ok_omitted_return_is_unit/main.solc @@ -0,0 +1,13 @@ +function nop() { + return; +} + +trait Hook { + function run(value: t); +} + +impl Hook { + function run(value: word) { + return; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/diagnostics.snap new file mode 100644 index 00000000..b567edf5 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/diagnostics.snap @@ -0,0 +1,6 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/main.solc +--- +no diagnostics diff --git a/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/main.solc b/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/main.solc new file mode 100644 index 00000000..7ec59d65 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/ok_uint256_binops_class_methods/main.solc @@ -0,0 +1,28 @@ +import std; +import std.dispatch; + +contract Uint256Binops { + function mul_u256(x: uint256, y: uint256) public returns (uint256) { + return x * y; + } + + function div_u256(x: uint256, y: uint256) public returns (uint256) { + return x / y; + } + + function mod_u256(x: uint256, y: uint256) public returns (uint256) { + return x % y; + } + + function band_u256(x: uint256, y: uint256) public returns (uint256) { + return x & y; + } + + function bxor_u256(x: uint256, y: uint256) public returns (uint256) { + return x ^ y; + } + + function bor_u256(x: uint256, y: uint256) public returns (uint256) { + return x | y; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/diagnostics.snap new file mode 100644 index 00000000..4d9cee66 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/omitted_forall_poly/main.solc +--- +error[SC0220]: top-level function must have complete type annotations + --> /main/main.solc:1:10 + | +1 | function id(x) { + | ^^ incomplete signature +2 | return x; +3 | } + | + = note: signature: function id(x) + = note: annotate every parameter (`name: Type`); omit `returns` for a function that returns no value diff --git a/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/main.solc b/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/main.solc new file mode 100644 index 00000000..40d70dca --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/omitted_forall_poly/main.solc @@ -0,0 +1,9 @@ +function id(x) { + return x; +} + +contract C { + function main() public returns (word) { + return id(42); + } +} diff --git a/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/diagnostics.snap new file mode 100644 index 00000000..915cbd5e --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/main.solc +--- +error[SC0201]: type mismatch: expected numeric, found () + --> /main/main.solc:2:10 + | +1 | function invalid() { +2 | return 1; + | ^ expression has mismatched type +3 | } + | + = note: expected type: numeric + = note: found type: () diff --git a/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/main.solc b/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/main.solc new file mode 100644 index 00000000..0bb3a6fd --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/omitted_return_rejects_value/main.solc @@ -0,0 +1,3 @@ +function invalid() { + return 1; +} diff --git a/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/diagnostics.snap new file mode 100644 index 00000000..4bb5701e --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/return_bool_mismatch/main.solc +--- +error[SC0201]: type mismatch: expected word, found bool + --> /main/main.solc:2:10 + | +1 | function f() returns (word) { +2 | return true; + | ^^^^ expression has mismatched type +3 | } + | + = note: expected type: word + = note: found type: bool diff --git a/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/main.solc new file mode 100644 index 00000000..1925917c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/return_bool_mismatch/main.solc @@ -0,0 +1,3 @@ +function f() returns (word) { + return true; +} diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/diagnostics.snap new file mode 100644 index 00000000..663dacc0 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/main.solc +--- +error[SC0108]: duplicate declaration `Choice.Same` in term namespace + --> /main/main.solc:1:27 + | +1 | enum Choice { Same(word), Same(bool) }; + | ---- ^^^^ duplicate declaration + | | + | previous declaration +2 | +3 | function ambiguous() returns (Choice) { + | diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/main.solc b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/main.solc new file mode 100644 index 00000000..2c63ddb1 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_ambiguous/main.solc @@ -0,0 +1,5 @@ +enum Choice { Same(word), Same(bool) }; + +function ambiguous() returns (Choice) { + return Choice.Same(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/diagnostics.snap new file mode 100644 index 00000000..65c56eab --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/diagnostics.snap @@ -0,0 +1,20 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/main.solc +--- +error[SC0201]: argument type mismatch in call to `Some` + --> /main/main.solc:5:19 + | +1 | enum Option { None, Some(word) }; + | ---- parameter 1 defined here +2 | +3 | function bad() returns (word) { +4 | let x: Option; +5 | x = Option.Some(true); + | ^^^^ argument has mismatched type +6 | return 0; + | + = note: expected `word` because parameter 1 of `Some` has type `word` + = note: found type: bool + = note: `Some` has signature `Some(word) returns (Option)` diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/main.solc new file mode 100644 index 00000000..528759c4 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_assignment_mismatch/main.solc @@ -0,0 +1,7 @@ +enum Option { None, Some(word) }; + +function bad() returns (word) { + let x: Option; + x = Option.Some(true); + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/diagnostics.snap new file mode 100644 index 00000000..1617f8f8 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/main.solc +--- +error[SC0106]: unqualified constructor: Some + --> /main/main.solc:4:11 + | +3 | function noContext() returns (word) { +4 | let x = Some(1); + | ^^^^ constructor must be qualified +5 | return 0; + | + = help: use `Option.Some` diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/main.solc b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/main.solc new file mode 100644 index 00000000..84518266 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_context/main.solc @@ -0,0 +1,8 @@ +enum Option { None, Some(word) }; + +function noContext() returns (word) { + let x = Some(1); + return 0; +} + +// migrate-syntax: keep-unqualified-constructor diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/diagnostics.snap new file mode 100644 index 00000000..0228b39a --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/main.solc +--- +error[SC0101]: undefined name: Some + --> /main/main.solc:4:16 + | +3 | function noMatch() returns (Other) { +4 | return Other.Some(1); + | ^^^^ unknown name +5 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/main.solc b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/main.solc new file mode 100644 index 00000000..eab6ce00 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/shorthand_constructor_no_match/main.solc @@ -0,0 +1,5 @@ +enum Other { Other }; + +function noMatch() returns (Other) { + return Other.Some(1); +} diff --git a/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/diagnostics.snap new file mode 100644 index 00000000..97109a72 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/diagnostics.snap @@ -0,0 +1,25 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: ABIDecoder: ABIDecode + --> /main/main.solc:6:3 + | +5 | contract C { +6 | constructor(value: Choice) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constraint originates here +7 | function main() returns (()) { return (); } + | + = note: no visible impl matches `ABIDecoder: ABIDecode` + = help: add a matching impl or strengthen the surrounding type context +--- + +error[SC0231]: ABI parameter cannot be represented in the ABI: Choice (user-defined ADTs are not supported by the canonical external ABI) + --> /main/main.solc:6:3 + | +5 | contract C { +6 | constructor(value: Choice) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsupported ABI type +7 | function main() returns (()) { return (); } + | diff --git a/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/main.solc b/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/main.solc new file mode 100644 index 00000000..38df6471 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/source_runtime_main_constructor_abi_error/main.solc @@ -0,0 +1,8 @@ +import std; + +enum Choice { Left(word), Right(word) }; + +contract C { + constructor(value: Choice) {} + function main() returns (()) { return (); } +} diff --git a/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/diagnostics.snap new file mode 100644 index 00000000..168c57ed --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: bool: Add + --> /main/main.solc:22:38 + | +21 | m: mapping(word => bool); +22 | function f(k: word) returns (()) { m[k] += true; } + | ^^^^^^^^^^^^ constraint originates here +23 | function main() returns (()) { return (); } + | + = note: no visible impl matches `bool: Add` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/main.solc b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/main.solc new file mode 100644 index 00000000..ac67409c --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_add_bool/main.solc @@ -0,0 +1,24 @@ +enum mapping { mapping(word) }; +enum uint256 { uint256(word) }; + +trait Add { + function add(l: t, r: t) returns (t); +} +trait Sub { + function sub(l: t, r: t) returns (t); +} +impl Add { + function add(l: word, r: word) returns (word) { return l; } +} +impl Sub { + function sub(l: word, r: word) returns (word) { return l; } +} +impl Add { + function add(l: uint256, r: uint256) returns (uint256) { return l; } +} + +contract C { + m: mapping(word => bool); + function f(k: word) returns (()) { m[k] += true; } + function main() returns (()) { return (); } +} diff --git a/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/diagnostics.snap new file mode 100644 index 00000000..ac50a134 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/main.solc +--- +error[SC0207]: cannot satisfy trait constraint: bool: Sub + --> /main/main.solc:22:38 + | +21 | m: mapping(word => bool); +22 | function f(k: word) returns (()) { m[k] -= true; } + | ^^^^^^^^^^^^ constraint originates here +23 | function main() returns (()) { return (); } + | + = note: no visible impl matches `bool: Sub` + = help: add a matching impl or strengthen the surrounding type context diff --git a/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/main.solc b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/main.solc new file mode 100644 index 00000000..ed5f644b --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/storage_mapping_compound_sub_bool/main.solc @@ -0,0 +1,24 @@ +enum mapping { mapping(word) }; +enum uint256 { uint256(word) }; + +trait Add { + function add(l: t, r: t) returns (t); +} +trait Sub { + function sub(l: t, r: t) returns (t); +} +impl Add { + function add(l: word, r: word) returns (word) { return l; } +} +impl Sub { + function sub(l: word, r: word) returns (word) { return l; } +} +impl Add { + function add(l: uint256, r: uint256) returns (uint256) { return l; } +} + +contract C { + m: mapping(word => bool); + function f(k: word) returns (()) { m[k] -= true; } + function main() returns (()) { return (); } +} diff --git a/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/diagnostics.snap new file mode 100644 index 00000000..b5070f77 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/main.solc +--- +error[SC0243]: type synonym expansion exceeded 16384 type nodes + --> /main/main.solc:14:7 + | +13 | alias T12 = (T11, T11); +14 | alias T13 = (T12, T12); + | ^^^ type alias expansion starts here +15 | + | diff --git a/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/main.solc b/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/main.solc new file mode 100644 index 00000000..51a1f3fb --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/type_alias_expansion_limit/main.solc @@ -0,0 +1,18 @@ +alias T0 = word; +alias T1 = (T0, T0); +alias T2 = (T1, T1); +alias T3 = (T2, T2); +alias T4 = (T3, T3); +alias T5 = (T4, T4); +alias T6 = (T5, T5); +alias T7 = (T6, T6); +alias T8 = (T7, T7); +alias T9 = (T8, T8); +alias T10 = (T9, T9); +alias T11 = (T10, T10); +alias T12 = (T11, T11); +alias T13 = (T12, T12); + +function use_bomb(x: T13) returns (T13) { + return x; +} diff --git a/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/diagnostics.snap new file mode 100644 index 00000000..9e5fb324 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/main.solc +--- +error[SC0299]: Invalid number of type arguments! + --> /main/main.solc:3:17 + | +2 | +3 | function f(x: P>) returns (word) { + | ^^^^^^^^^^ diagnostic reported here +4 | return 0; + | + = note: Type word is expected to have 0 type arguments + = note: but, type word has 1 arguments diff --git a/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/main.solc b/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/main.solc new file mode 100644 index 00000000..ea9c82ad --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/type_annotation_kind_mismatch/main.solc @@ -0,0 +1,5 @@ +enum P { Mk(a) }; + +function f(x: P>) returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/diagnostics.snap new file mode 100644 index 00000000..9688bd62 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/main.solc +--- +error[SC0299]: Invalid number of type arguments! + --> /main/main.solc:3:15 + | +2 | +3 | function f(x: P) returns (word) { + | ^ diagnostic reported here +4 | return 0; + | + = note: Type P is expected to have 1 type arguments + = note: but, type P has 0 arguments diff --git a/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/main.solc b/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/main.solc new file mode 100644 index 00000000..a0b72125 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unary_type_unapplied_signature/main.solc @@ -0,0 +1,5 @@ +enum P { Mk(a) }; + +function f(x: P) returns (word) { + return 0; +} diff --git a/crates/uitest/tests/fixtures/typeck/unknown_field/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/unknown_field/diagnostics.snap new file mode 100644 index 00000000..54853535 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unknown_field/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/unknown_field/main.solc +--- +error[SC0205]: cannot resolve field `foo` + --> /main/main.solc:2:12 + | +1 | function f(x: word) returns (word) { +2 | return x.foo; + | ^^^ unknown field +3 | } + | + = help: check that the receiver has this field or constructor path diff --git a/crates/uitest/tests/fixtures/typeck/unknown_field/main.solc b/crates/uitest/tests/fixtures/typeck/unknown_field/main.solc new file mode 100644 index 00000000..6e85e9ef --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unknown_field/main.solc @@ -0,0 +1,3 @@ +function f(x: word) returns (word) { + return x.foo; +} diff --git a/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/diagnostics.snap new file mode 100644 index 00000000..c41f8168 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/diagnostics.snap @@ -0,0 +1,14 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/unreachable_match_arm/main.solc +--- +warning[SC0303]: unreachable match arm + --> /main/main.solc:6:3 + | +5 | default { return 0; } +6 | case Flag.Off { return 1; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ this arm is unreachable +7 | } + | + = note: this arm is covered by previous match arms diff --git a/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/main.solc b/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/main.solc new file mode 100644 index 00000000..25f85b00 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/unreachable_match_arm/main.solc @@ -0,0 +1,8 @@ +enum Flag { Off, On }; + +function pick(x: Flag) returns (word) { + match (x) { + default { return 0; } + case Flag.Off { return 1; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/diagnostics.snap new file mode 100644 index 00000000..d61b9a1a --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/diagnostics.snap @@ -0,0 +1,51 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/main.solc +--- +error[SC0231]: function `echo` declared public ABI for contract `C` cannot use visible manual `ABIAttribs` evidence + --> /main/main.solc:22:3 + | +21 | contract C { +22 | function echo(value: word) public returns (word) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ external ABI evidence must be compiler-owned and canonical +23 | } + | + = note: impl `ABIAttribs` can override canonical `ABIAttribs` behavior + = help: remove the visible manual ABI impl or keep this declaration out of the external ABI +--- + +error[SC0231]: function `echo` declared public ABI for contract `C` cannot use visible manual `ABIDecode` evidence + --> /main/main.solc:22:3 + | +21 | contract C { +22 | function echo(value: word) public returns (word) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ external ABI evidence must be compiler-owned and canonical +23 | } + | + = note: impl `ABIDecode` can override canonical `ABIDecode` behavior + = help: remove the visible manual ABI impl or keep this declaration out of the external ABI +--- + +error[SC0231]: function `echo` declared public ABI for contract `C` cannot use visible manual `ABIEncode` evidence + --> /main/main.solc:22:3 + | +21 | contract C { +22 | function echo(value: word) public returns (word) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ external ABI evidence must be compiler-owned and canonical +23 | } + | + = note: impl `ABIEncode` can override canonical `ABIEncode` behavior + = help: remove the visible manual ABI impl or keep this declaration out of the external ABI +--- + +error[SC0231]: function `echo` declared public ABI for contract `C` cannot use visible manual `SigString` evidence + --> /main/main.solc:22:3 + | +21 | contract C { +22 | function echo(value: word) public returns (word) { return value; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ external ABI evidence must be compiler-owned and canonical +23 | } + | + = note: impl `SigString` can override canonical `SigString` behavior + = help: remove the visible manual ABI impl or keep this declaration out of the external ABI diff --git a/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/main.solc b/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/main.solc new file mode 100644 index 00000000..47b96045 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/visible_manual_std_abi_instances/main.solc @@ -0,0 +1,23 @@ +import std; +import std.dispatch; + +impl ABIAttribs { + function headSize(p: Proxy) returns (word) { return 32; } + function isStatic(p: Proxy) returns (bool) { return true; } +} + +impl ABIEncode { + function encodeInto(x: word, base: word, offset: word, tail: word) returns (word) { return tail; } +} + +impl ABIDecode, word> { + function decode(d: ABIDecoder, offset: word) returns (word) { return 0; } +} + +impl SigString { + function sigStr(p: Proxy) returns (string) { return "uint256"; } +} + +contract C { + function echo(value: word) public returns (word) { return value; } +} diff --git a/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/diagnostics.snap new file mode 100644 index 00000000..3408942f --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/main.solc +--- +error[SC0201]: type mismatch: expected mapping(address => uint256), found mapping(address => uint256) storage + --> /main/main.solc:10:12 + | + 9 | function leak() returns (mapping(address => uint256)) { +10 | return balances; + | ^^^^^^^^ expression has mismatched type +11 | } + | + = note: expected type: mapping(address => uint256) + = note: found type: mapping(address => uint256) storage diff --git a/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/main.solc b/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/main.solc new file mode 100644 index 00000000..0967e182 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/whole_mapping_private_full/main.solc @@ -0,0 +1,16 @@ +enum address { address(word) }; +enum uint256 { uint256(word) }; +enum mapping { mapping(word) }; +enum storage { storage(word) }; + +contract C { + balances: mapping(address => uint256); + + function leak() returns (mapping(address => uint256)) { + return balances; + } + + function main() returns (word) { + return 0; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/diagnostics.snap new file mode 100644 index 00000000..ae65a480 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/main.solc +--- +error[SC0302]: non-exhaustive pattern match + --> /main/main.solc:2:10 + | +1 | function pick(x: word) returns (word) { +2 | match (x) { + | ^ non-exhaustive match +3 | case 0 { return 0; } + | + = note: missing case: _ + = note: help: add a clause that covers the missing case diff --git a/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/main.solc b/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/main.solc new file mode 100644 index 00000000..66906078 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/word_literals_nonexhaustive/main.solc @@ -0,0 +1,6 @@ +function pick(x: word) returns (word) { + match (x) { + case 0 { return 0; } + case 1 { return 1; } + } +} diff --git a/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/diagnostics.snap new file mode 100644 index 00000000..42a48bd7 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/diagnostics.snap @@ -0,0 +1,15 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/main.solc +--- +error[SC0203]: Yul assignment expects 3 arguments, but 2 were provided + --> /main/main.solc:11:7 + | +10 | } +11 | x, y, z := pair() + | ^^^^^^^^^^^^^^^^^ wrong number of arguments +12 | } + | + = note: expected 3 arguments + = note: found 2 arguments diff --git a/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/main.solc b/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/main.solc new file mode 100644 index 00000000..5e67a19d --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_multi_return_arity/main.solc @@ -0,0 +1,15 @@ +contract YulMultiRetBad { + function main() public returns (word) { + let x: word; + let y: word; + let z: word; + assembly { + function pair() -> a, b { + a := 1 + b := 2 + } + x, y, z := pair() + } + return x; + } +} diff --git a/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/diagnostics.snap new file mode 100644 index 00000000..5d600830 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/diagnostics.snap @@ -0,0 +1,13 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/main.solc +--- +error[SC0204]: Yul reference `b` requires word type, got bool + --> /main/main.solc:3:14 + | +2 | let b: bool = false; +3 | assembly { b := add(1, 1) } + | ^ Yul reference has non-word type +4 | if (b) { return 1; } else { return 0; } + | diff --git a/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/main.solc b/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/main.solc new file mode 100644 index 00000000..99834149 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_non_word_sail_variable/main.solc @@ -0,0 +1,5 @@ +function main() returns (word) { + let b: bool = false; + assembly { b := add(1, 1) } + if (b) { return 1; } else { return 0; } +} diff --git a/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/diagnostics.snap b/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/diagnostics.snap new file mode 100644 index 00000000..2f3efaf3 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/diagnostics.snap @@ -0,0 +1,49 @@ +--- +source: crates/test-utils/src/lib.rs +expression: rendered +input_file: crates/uitest/tests/fixtures/typeck/yul_opcode_errors/main.solc +--- +error[SC0203]: Yul call `add` expects 2 arguments, but 1 was provided + --> /main/main.solc:4:16 + | +3 | assembly { +4 | let one := add(1) + | ^^^^^^ wrong number of arguments +5 | let two := add("bad", 1) + | + = note: expected 2 arguments + = note: found 1 argument +--- + +error[SC0201]: type mismatch: expected word, found string + --> /main/main.solc:5:20 + | +4 | let one := add(1) +5 | let two := add("bad", 1) + | ^^^^^ expression has mismatched type +6 | x := mstore(1, 1) + | + = note: expected type: word + = note: found type: string +--- + +error[SC0203]: Yul assignment expects 1 argument, but 0 were provided + --> /main/main.solc:6:5 + | +5 | let two := add("bad", 1) +6 | x := mstore(1, 1) + | ^^^^^^^^^^^^^^^^^ wrong number of arguments +7 | x := add(missing, 1) + | + = note: expected 1 argument + = note: found 0 arguments +--- + +error[SC0211]: unknown Yul identifier or function: missing + --> /main/main.solc:7:14 + | +6 | x := mstore(1, 1) +7 | x := add(missing, 1) + | ^^^^^^^ unknown Yul name +8 | } + | diff --git a/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/main.solc b/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/main.solc new file mode 100644 index 00000000..1a4d35f6 --- /dev/null +++ b/crates/uitest/tests/fixtures/typeck/yul_opcode_errors/main.solc @@ -0,0 +1,10 @@ +function badYul() returns (word) { + let x: word; + assembly { + let one := add(1) + let two := add("bad", 1) + x := mstore(1, 1) + x := add(missing, 1) + } + return x; +} diff --git a/crates/vfs/Cargo.toml b/crates/vfs/Cargo.toml new file mode 100644 index 00000000..ae47e75d --- /dev/null +++ b/crates/vfs/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "solcore-vfs" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[lib] +name = "solcore_vfs" + +[dependencies] +hir = { workspace = true } +hir-ty = { workspace = true } +nameres = { workspace = true } +parser = { workspace = true } +rustc-hash = { workspace = true } +salsa = { workspace = true } +url = { workspace = true } diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs new file mode 100644 index 00000000..601b9e3d --- /dev/null +++ b/crates/vfs/src/lib.rs @@ -0,0 +1,1385 @@ +//! In-memory analysis host for Solcore compiler front-end queries. +//! +//! The VFS uses virtual absolute paths instead of the process filesystem: +//! `/main` for user files, `/std` for the embedded standard library, and +//! `/ext/` for optional external libraries. Source files are still backed +//! by the existing Salsa [`hir::input::SourceFile`] input so edits update the +//! same incremental compiler graph used by the native driver. + +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + path::{Path, PathBuf}, +}; + +use hir::{ + diag::{Applicability as HirApplicability, DiagnosticLevel}, + input::SourceFile, +}; +use nameres::{ + LibraryId, ModuleFileSnapshot, ModuleFsSnapshot, ModuleId, ModuleKey, ModuleTree, + module_id_from_key, module_key_for_path, resolve_module_path_candidate, +}; +use rustc_hash::{FxHashMap, FxHashSet}; +use salsa::{Durability, Setter}; +use url::Url; + +/// Virtual root for user sources. +pub const MAIN_ROOT: &str = "/main"; +/// Virtual root for the embedded Solcore standard library. +pub const STD_ROOT: &str = "/std"; +/// Virtual root containing named external libraries. +pub const EXT_ROOT: &str = "/ext"; + +/// Embedded standard-library files, mounted under [`STD_ROOT`]. +pub const STD_FILES: &[(&str, &str)] = &[ + ( + "std.solc", + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../std/std.solc")), + ), + ( + "dispatch.solc", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../std/dispatch.solc" + )), + ), + ( + "opcodes.solc", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../std/opcodes.solc" + )), + ), + ( + "Generic.solc", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../std/Generic.solc" + )), + ), + ( + "ABIGeneric.solc", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../std/ABIGeneric.solc" + )), + ), +]; + +/// Concrete Salsa database used by the in-memory analysis host. +#[salsa::db] +pub struct AnalysisHost { + storage: salsa::Storage, + module_tree: Option, + module_fs_snapshot: Option, + module_file_snapshot: Option, + module_files: BTreeMap, + files: FxHashMap, + tombstones: FxHashMap, +} + +impl AnalysisHost { + /// Creates an empty host with virtual `/main` and `/std` roots configured. + pub fn new() -> Self { + Self::with_storage(salsa::Storage::new(None)) + } + + fn with_storage(storage: salsa::Storage) -> Self { + let mut host = Self { + storage, + module_tree: None, + module_fs_snapshot: None, + module_file_snapshot: None, + module_files: BTreeMap::new(), + files: FxHashMap::default(), + tombstones: FxHashMap::default(), + }; + host.initialize_roots(BTreeMap::new()); + host.module_file_snapshot = Some(ModuleFileSnapshot::new(&host, BTreeMap::new())); + host.rebuild_module_fs_snapshot(); + host + } + + /// Adds or replaces an in-memory file at an absolute virtual path. + pub fn set_virtual_file(&mut self, path: impl Into, contents: String) -> SourceFile { + let (file, changes) = + self.set_virtual_file_deferred(path.into(), contents, Durability::LOW); + self.finish_file_changes(changes); + file + } + + /// Removes an in-memory file at an absolute virtual path. + pub fn remove_virtual_file(&mut self, path: impl Into) { + let changes = self.remove_virtual_file_deferred(path.into()); + self.finish_file_changes(changes); + } + + /// Returns the source file stored at `path`, if present. + pub fn source_file(&self, path: impl AsRef) -> Option { + self.files.get(path.as_ref()).copied() + } + + /// Seeds `/std` with the embedded standard library. + pub fn seed_std(&mut self) { + let mut changes = FileChanges::default(); + for (name, contents) in STD_FILES { + let (_, file_changes) = self.set_virtual_file_deferred( + PathBuf::from(STD_ROOT).join(name), + (*contents).to_owned(), + Durability::HIGH, + ); + changes.merge(file_changes); + } + self.finish_file_changes(changes); + } + + fn initialize_roots(&mut self, external_roots: BTreeMap) { + let main_root = PathBuf::from(MAIN_ROOT); + let std_root = PathBuf::from(STD_ROOT); + self.module_tree = Some( + ModuleTree::builder(main_root, std_root, external_roots) + .durability(Durability::HIGH) + .new(self), + ); + } + + fn ensure_external_root(&mut self, name: &str) { + let root = external_root(name); + let tree = self + .module_tree + .expect("AnalysisHost module tree is initialized"); + if tree.external_roots(self).get(name) == Some(&root) { + return; + } + let mut external_roots = tree.external_roots(self).clone(); + external_roots.insert(name.to_owned(), root); + tree.set_external_roots(self).to(external_roots); + } + + fn register_module_file(&mut self, path: &Path, file: SourceFile) -> bool { + if !is_solcore_module_path(path) { + return false; + } + if let Some(key) = self.module_key_for_virtual_path(path) { + return self.set_module_file(key, file); + } + false + } + + fn set_module_file(&mut self, key: ModuleKey, file: SourceFile) -> bool { + self.module_files.insert(key, file) != Some(file) + } + + fn remove_module_file(&mut self, key: &ModuleKey) -> bool { + self.module_files.remove(key).is_some() + } + + fn module_key_for_virtual_path(&self, path: &Path) -> Option { + let tree = self + .module_tree + .expect("AnalysisHost module tree is initialized"); + module_key_for_path(LibraryId::Main, tree.main_root(self), path) + .or_else(|| module_key_for_path(LibraryId::Std, tree.std_root(self), path)) + .or_else(|| { + tree.external_roots(self).iter().find_map(|(name, root)| { + module_key_for_path(LibraryId::External(name.clone()), root, path) + }) + }) + } + + fn rebuild_module_fs_snapshot(&mut self) { + let (existing_files, sibling_stems) = module_fs_snapshot_from_paths(self.files.keys()); + if let Some(snapshot) = self.module_fs_snapshot { + if snapshot.existing_files(self) != &existing_files { + snapshot.set_existing_files(self).to(existing_files); + } + if snapshot.sibling_stems(self) != &sibling_stems { + snapshot.set_sibling_stems(self).to(sibling_stems); + } + } else { + self.module_fs_snapshot = + Some(ModuleFsSnapshot::new(self, existing_files, sibling_stems)); + } + } + + fn sync_module_file_snapshot(&mut self) { + let files = self.module_files.clone(); + if let Some(snapshot) = self.module_file_snapshot { + if snapshot.files(self) != &files { + snapshot.set_files(self).to(files); + } + } else { + self.module_file_snapshot = Some(ModuleFileSnapshot::new(self, files)); + } + } + + fn set_virtual_file_deferred( + &mut self, + path: PathBuf, + contents: String, + durability: Durability, + ) -> (SourceFile, FileChanges) { + let path = normalize_absolute_path(path); + let (file, file_set_changed) = if let Some(file) = self.files.get(&path).copied() { + if file.content(self).as_deref() != Some(contents.as_str()) { + file.set_content(self) + .with_durability(durability) + .to(Some(contents)); + } + (file, false) + } else if let Some(file) = self.tombstones.remove(&path) { + file.set_content(self) + .with_durability(durability) + .to(Some(contents)); + self.files.insert(path.clone(), file); + (file, true) + } else { + let file = source_file_for_virtual_path(self, &path, contents, durability); + self.files.insert(path.clone(), file); + (file, true) + }; + let module_files_changed = self.register_module_file(&path, file); + ( + file, + FileChanges { + file_set_changed, + module_files_changed, + }, + ) + } + + fn remove_virtual_file_deferred(&mut self, path: PathBuf) -> FileChanges { + let path = normalize_absolute_path(path); + let removed_file = self.files.remove(&path); + let file_set_changed = if let Some(file) = removed_file { + file.set_content(self).to(None); + self.tombstones.insert(path.clone(), file); + true + } else { + false + }; + let module_files_changed = removed_file.is_some_and(|file| { + is_solcore_module_path(&path) + && self.module_key_for_virtual_path(&path).is_some_and(|key| { + self.module_files.get(&key) == Some(&file) && self.remove_module_file(&key) + }) + }); + FileChanges { + file_set_changed, + module_files_changed, + } + } + + fn finish_file_changes(&mut self, changes: FileChanges) { + if changes.module_files_changed { + self.sync_module_file_snapshot(); + } + if changes.file_set_changed { + self.rebuild_module_fs_snapshot(); + } + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct FileChanges { + file_set_changed: bool, + module_files_changed: bool, +} + +impl FileChanges { + fn merge(&mut self, other: Self) { + self.file_set_changed |= other.file_set_changed; + self.module_files_changed |= other.module_files_changed; + } +} + +impl Default for AnalysisHost { + fn default() -> Self { + Self::new() + } +} + +#[salsa::db] +impl salsa::Database for AnalysisHost {} + +#[salsa::db] +impl hir::Db for AnalysisHost { + fn def_location_table<'db>( + &'db self, + file: SourceFile, + ) -> &'db hir::anchor::DefLocationTable<'db> { + parser::parse_file_to_hir(self, file).def_locations(self) + } +} + +#[salsa::db] +impl parser::Db for AnalysisHost {} + +#[salsa::db] +impl nameres::Db for AnalysisHost { + fn module_tree(&self) -> ModuleTree { + self.module_tree + .expect("AnalysisHost module tree is initialized before use") + } + + fn module_fs_snapshot(&self) -> ModuleFsSnapshot { + self.module_fs_snapshot + .expect("AnalysisHost module filesystem snapshot is initialized before use") + } + + fn module_file_snapshot(&self) -> ModuleFileSnapshot { + self.module_file_snapshot + .expect("AnalysisHost module file snapshot is initialized before use") + } + + fn module_file<'db>(&'db self, module: ModuleId<'db>) -> Option { + self.module_file_snapshot() + .files(self) + .get(&module.key(self)) + .copied() + } +} + +#[salsa::db] +impl hir_ty::Db for AnalysisHost {} + +/// High-level in-memory workspace for analysis and editor-style queries. +pub struct Workspace { + host: AnalysisHost, + entry_path: Option, +} + +/// One file-system mutation applied by [`Workspace::apply_file_changes`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum WorkspaceFileChange { + /// Add or replace a user file below `/main`. + Set { path: String, contents: String }, + /// Remove a user file below `/main`. + Remove { path: String }, + /// Add or replace a file in a named external library. + SetExternal { + library: String, + path: String, + contents: String, + }, + /// Remove a file from a named external library. + RemoveExternal { library: String, path: String }, +} + +impl Workspace { + /// Creates a workspace with the embedded standard library mounted at + /// `/std`. + pub fn new() -> Self { + let mut host = AnalysisHost::new(); + host.seed_std(); + Self { + host, + entry_path: None, + } + } + + /// Adds or replaces a user file under `/main`. + /// + /// Both `main.solc` and `/main/main.solc` refer to `/main/main.solc`. + pub fn set_file(&mut self, path: &str, contents: String) { + self.apply_file_changes([WorkspaceFileChange::Set { + path: path.to_owned(), + contents, + }]); + } + + /// Removes a user file under `/main`. + pub fn remove_file(&mut self, path: &str) { + self.apply_file_changes([WorkspaceFileChange::Remove { + path: path.to_owned(), + }]); + } + + /// Adds or replaces a file in a named external library under `/ext/`. + pub fn set_external_file(&mut self, library: &str, path: &str, contents: String) { + self.apply_file_changes([WorkspaceFileChange::SetExternal { + library: library.to_owned(), + path: path.to_owned(), + contents, + }]); + } + + /// Removes a file from a named external library under `/ext/`. + pub fn remove_external_file(&mut self, library: &str, path: &str) { + self.apply_file_changes([WorkspaceFileChange::RemoveExternal { + library: library.to_owned(), + path: path.to_owned(), + }]); + } + + /// Applies several user/external file changes as one workspace update. + /// + /// The module registry and filesystem snapshot are rebuilt at most once, + /// and reachable-module loading runs once after all changes are visible. + pub fn apply_file_changes(&mut self, changes: impl IntoIterator) { + let mut accumulated = FileChanges::default(); + for change in changes { + let current = match change { + WorkspaceFileChange::Set { path, contents } => { + self.host + .set_virtual_file_deferred(main_path(&path), contents, Durability::LOW) + .1 + } + WorkspaceFileChange::Remove { path } => { + self.host.remove_virtual_file_deferred(main_path(&path)) + } + WorkspaceFileChange::SetExternal { + library, + path, + contents, + } => { + let name = normalize_external_name(&library); + self.host.ensure_external_root(&name); + self.host + .set_virtual_file_deferred( + external_path(&name, &path), + contents, + Durability::LOW, + ) + .1 + } + WorkspaceFileChange::RemoveExternal { library, path } => { + let name = normalize_external_name(&library); + self.host.ensure_external_root(&name); + self.host + .remove_virtual_file_deferred(external_path(&name, &path)) + } + }; + accumulated.merge(current); + } + self.host.finish_file_changes(accumulated); + self.load_entry_modules(); + } + + /// Selects the entry module under `/main`. + pub fn set_entry(&mut self, path: &str) { + self.entry_path = Some(main_path(path)); + self.load_entry_modules(); + } + + /// Returns the underlying Salsa database for richer downstream queries. + pub fn db(&self) -> &AnalysisHost { + &self.host + } + + /// Returns a mutable database handle for advanced callers that need to + /// update virtual files directly. + pub fn db_mut(&mut self) -> &mut AnalysisHost { + &mut self.host + } + + /// Returns the resolved entry module, if the entry file exists under + /// `/main`. + pub fn entry_module(&self) -> Option> { + let path = self.entry_path.as_ref()?; + self.module_for_main_path(path) + } + + fn module_for_main_path(&self, path: &Path) -> Option> { + let key = self.main_key_for_path(path)?; + self.host + .module_files + .contains_key(&key) + .then(|| module_id_from_key(&self.host, &key)) + .or_else(|| { + self.host + .files + .contains_key(path) + .then(|| module_id_from_key(&self.host, &key)) + }) + } + + /// Returns lowered, sorted, and deduplicated compiler diagnostics. + pub fn raw_diagnostics(&self) -> Vec { + let Some(entry) = self.entry_module() else { + return Vec::new(); + }; + self.raw_diagnostics_for_module(entry) + } + + /// Returns compiler diagnostics for an alternate `/main` entry without + /// mutating the workspace or its Salsa inputs. + pub fn raw_diagnostics_for_entry(&self, path: &str) -> Vec { + let path = main_path(path); + let Some(entry) = self.module_for_main_path(&path) else { + return Vec::new(); + }; + self.raw_diagnostics_for_module(entry) + } + + fn raw_diagnostics_for_module(&self, entry: ModuleId<'_>) -> Vec { + hir_ty::collect_frontend_diagnostics(&self.host, entry) + } + + /// Returns diagnostics as a serde-free owned mirror suitable for adapters. + pub fn diagnostics(&self) -> Vec { + self.raw_diagnostics() + .into_iter() + .map(|diagnostic| Diagnostic::from_hir(&self.host, diagnostic)) + .collect() + } + + /// Returns owned diagnostics for an alternate `/main` entry without + /// changing the selected workspace entry. + pub fn diagnostics_for_entry(&self, path: &str) -> Vec { + self.raw_diagnostics_for_entry(path) + .into_iter() + .map(|diagnostic| Diagnostic::from_hir(&self.host, diagnostic)) + .collect() + } + + fn entry_key(&self) -> Option { + let path = self.entry_path.as_ref()?; + self.main_key_for_path(path) + } + + fn main_key_for_path(&self, path: &Path) -> Option { + let tree = self + .host + .module_tree + .expect("AnalysisHost module tree is initialized"); + module_key_for_path(LibraryId::Main, tree.main_root(&self.host), path) + } + + fn load_entry_modules(&mut self) { + let Some(key) = self.entry_key() else { + return; + }; + load_reachable_modules(&mut self.host, key); + } +} + +impl Default for Workspace { + fn default() -> Self { + Self::new() + } +} + +/// Lowered compiler diagnostic preserved for exact rendering. +pub type RawDiagnostic = hir::diag::Diagnostic; + +/// Plain diagnostic severity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DiagnosticSeverity { + /// Compilation-blocking error. + Error, + /// Recoverable warning. + Warning, + /// Informational note. + Note, + /// Suggested remediation or help. + Help, +} + +/// Alias for the plain diagnostic severity used by adapters. +pub type Severity = DiagnosticSeverity; + +/// Byte range in a source file. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiagRange { + /// File URL string. + pub file_url: String, + /// Inclusive start byte offset. + pub start: u32, + /// Exclusive end byte offset. + pub end: u32, +} + +/// Backward-compatible alias for a diagnostic byte range. +pub type DiagnosticSpan = DiagRange; + +/// Diagnostic label with an absolute byte range. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiagLabel { + /// Label byte range. + pub range: DiagRange, + /// Label message, when available. + pub message: Option, + /// Whether this is the primary label. + pub is_primary: bool, +} + +/// Backward-compatible alias for a diagnostic label. +pub type DiagnosticLabel = DiagLabel; + +/// Confidence level for applying a diagnostic suggestion. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SuggestionApplicability { + /// The edit can be applied mechanically. + MachineApplicable, + /// The edit is plausible but may need user review. + MaybeIncorrect, + /// The edit contains placeholders requiring user input. + HasPlaceholders, + /// The compiler did not classify the edit. + Unspecified, +} + +/// One replacement edit belonging to a diagnostic suggestion. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiagnosticTextEdit { + /// Absolute source range to replace. + pub range: DiagRange, + /// Replacement source text. + pub replacement: String, +} + +/// Structured quick-fix suggestion emitted by the compiler. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DiagnosticSuggestion { + /// User-facing action title. + pub title: String, + /// Confidence level for applying the edit. + pub applicability: SuggestionApplicability, + /// All edits required by this suggestion. + pub edits: Vec, +} + +/// Serde-free owned diagnostic mirror for playground and LSP adapters. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Diagnostic { + /// Diagnostic severity. + pub severity: DiagnosticSeverity, + /// Optional diagnostic code such as `SC0101`. + pub code: Option, + /// Human-readable headline message. + pub message: String, + /// Primary label range, when the compiler provided a source label. + pub primary: Option, + /// All source labels, including the primary label. + pub labels: Vec, + /// Additional note text. + pub notes: Vec, + /// Additional help text. + pub helps: Vec, + /// Structured source edits that can resolve the diagnostic. + pub suggestions: Vec, +} + +impl Diagnostic { + /// Converts a lowered HIR diagnostic into this serde-free owned form. + /// + /// Source spans are resolved against `db` at this adapter boundary, so any + /// database implementing [`hir::Db`] can reuse the conversion. `diagnostic` + /// and `db` must originate from the same Salsa storage and compatible + /// revision so tracked definition anchors can be resolved. + /// + /// # Panics + /// + /// Panics if a diagnostic span cannot be resolved against `db`. + pub fn from_hir(db: &dyn hir::Db, diagnostic: RawDiagnostic) -> Self { + let labels = diagnostic + .labels + .iter() + .map(|label| { + let absolute = label.span().resolve_to_absolute(db); + DiagLabel { + range: range_from_absolute_span(db, absolute), + message: label.message().map(str::to_owned), + is_primary: label.is_primary(), + } + }) + .collect::>(); + let primary = labels + .iter() + .find(|label| label.is_primary) + .or_else(|| labels.first()) + .map(|label| label.range.clone()); + let suggestions = diagnostic + .suggestions + .iter() + .map(|suggestion| DiagnosticSuggestion { + title: suggestion.title.clone(), + applicability: suggestion.applicability.into(), + edits: suggestion + .edits + .iter() + .map(|edit| DiagnosticTextEdit { + range: range_from_absolute_span(db, edit.span.resolve_to_absolute(db)), + replacement: edit.replacement.clone(), + }) + .collect(), + }) + .collect(); + Self { + severity: diagnostic.level.into(), + code: diagnostic.code, + message: diagnostic.message, + primary, + labels, + notes: diagnostic.notes, + helps: diagnostic.helps, + suggestions, + } + } +} + +impl From for SuggestionApplicability { + fn from(applicability: HirApplicability) -> Self { + match applicability { + HirApplicability::MachineApplicable => Self::MachineApplicable, + HirApplicability::MaybeIncorrect => Self::MaybeIncorrect, + HirApplicability::HasPlaceholders => Self::HasPlaceholders, + HirApplicability::Unspecified => Self::Unspecified, + } + } +} + +impl From for DiagnosticSeverity { + fn from(level: DiagnosticLevel) -> Self { + match level { + DiagnosticLevel::Error => Self::Error, + DiagnosticLevel::Warning => Self::Warning, + DiagnosticLevel::Note => Self::Note, + DiagnosticLevel::Help => Self::Help, + } + } +} + +fn range_from_absolute_span(db: &dyn hir::Db, span: hir::diag::AbsoluteSpan) -> DiagRange { + let file = span.file(); + DiagRange { + file_url: file.url(db).as_str().to_owned(), + start: span.start().as_u32(), + end: span.end().as_u32(), + } +} + +/// Loads all modules reachable from `entry` using only the host's in-memory +/// file map. +pub fn load_reachable_modules(host: &mut AnalysisHost, entry: ModuleKey) { + let mut queue = VecDeque::from([entry]); + let mut visited = FxHashSet::default(); + let mut module_files_changed = false; + + while let Some(key) = queue.pop_front() { + if !visited.insert(key.clone()) { + continue; + } + let Some(file) = host.module_files.get(&key).copied() else { + continue; + }; + let targets = { + let module = module_id_from_key(&*host, &key); + let refs = nameres::module_imports(&*host, file); + refs.import_refs + .into_iter() + .chain(refs.export_refs) + .filter_map(|path| { + let resolved = resolve_module_path_candidate(&*host, module, &path).ok()?; + Some((resolved.module.key(&*host), resolved.file_path)) + }) + .collect::>() + }; + + for (target_key, file_path) in targets { + if !host.module_files.contains_key(&target_key) + && let Some(file) = host.files.get(&file_path).copied() + { + module_files_changed |= host.set_module_file(target_key.clone(), file); + } + if host.module_files.contains_key(&target_key) { + queue.push_back(target_key); + } + } + } + if module_files_changed { + host.sync_module_file_snapshot(); + } +} + +fn source_file_for_virtual_path( + db: &AnalysisHost, + path: &Path, + source: String, + durability: Durability, +) -> SourceFile { + let path = path + .to_str() + .expect("virtual paths are constructed from UTF-8 strings"); + let mut url = Url::parse("file:///").expect("file URL base"); + url.set_path(path); + SourceFile::builder(url, Some(source)) + .durability(durability) + .new(db) +} + +fn module_fs_snapshot_from_paths<'a>( + paths: impl IntoIterator, +) -> (BTreeSet, BTreeMap>) { + let mut existing_files = BTreeSet::new(); + let mut sibling_stems = BTreeMap::>::new(); + for path in paths { + if path.extension().and_then(|extension| extension.to_str()) != Some("solc") { + continue; + } + existing_files.insert(path.clone()); + if let (Some(parent), Some(stem)) = ( + path.parent(), + path.file_stem().and_then(|stem| stem.to_str()), + ) { + sibling_stems + .entry(parent.to_path_buf()) + .or_default() + .insert(stem.to_owned()); + } + } + let sibling_stems = sibling_stems + .into_iter() + .map(|(parent, stems)| (parent, stems.into_iter().collect())) + .collect(); + (existing_files, sibling_stems) +} + +fn is_solcore_module_path(path: &Path) -> bool { + path.extension().and_then(|extension| extension.to_str()) == Some("solc") +} + +fn normalize_absolute_path(path: PathBuf) -> PathBuf { + if path.is_absolute() { + path + } else { + PathBuf::from("/").join(path) + } +} + +fn main_path(path: &str) -> PathBuf { + let path = path.trim(); + if path == MAIN_ROOT || path.starts_with("/main/") { + PathBuf::from(path) + } else { + PathBuf::from(MAIN_ROOT).join(path.trim_start_matches('/')) + } +} + +fn external_root(name: &str) -> PathBuf { + PathBuf::from(EXT_ROOT).join(name) +} + +fn external_path(name: &str, path: &str) -> PathBuf { + let path = path.trim(); + let root = external_root(name); + if path == root.to_string_lossy() || path.starts_with(&format!("{}/", root.display())) { + PathBuf::from(path) + } else { + root.join(path.trim_start_matches('/')) + } +} + +fn normalize_external_name(name: &str) -> String { + name.trim().trim_start_matches('@').to_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + + fn host_with_execution_log() -> (AnalysisHost, Arc>>) { + let executed = Arc::new(Mutex::new(Vec::new())); + let host = AnalysisHost::with_storage(salsa::Storage::new(Some(Box::new({ + let executed = executed.clone(); + move |event| { + if let salsa::EventKind::WillExecute { database_key } = event.kind { + executed + .lock() + .expect("execution log lock") + .push(format!("{database_key:?}")); + } + } + })))); + (host, executed) + } + + fn take_executed(executed: &Mutex>) -> Vec { + std::mem::take(&mut *executed.lock().expect("execution log lock")) + } + + fn query_executions(events: &[String], query: &str) -> usize { + events.iter().filter(|event| event.contains(query)).count() + } + + fn workspace_with_main(source: &str) -> Workspace { + let mut workspace = Workspace::new(); + workspace.set_file("main.solc", source.to_owned()); + workspace.set_entry("main.solc"); + workspace + } + + fn messages(workspace: &Workspace) -> Vec { + workspace + .diagnostics() + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect() + } + + fn raw_messages(workspace: &Workspace) -> Vec { + workspace + .raw_diagnostics() + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect() + } + + fn driver_style_messages(source: &str) -> Vec { + let mut host = AnalysisHost::new(); + let path = main_path("main.solc"); + host.set_virtual_file(path.clone(), source.to_owned()); + let tree = host + .module_tree + .expect("AnalysisHost module tree is initialized"); + let key = + module_key_for_path(LibraryId::Main, tree.main_root(&host), &path).expect("entry key"); + load_reachable_modules(&mut host, key.clone()); + let entry = module_id_from_key(&host, &key); + let _ = nameres::resolve_reachable_full(&host, entry); + let mut diagnostics = nameres::reachable_diagnostics(&host, entry) + .iter() + .map(|diagnostic| diagnostic.lower(&host)) + .collect::>(); + diagnostics.extend( + hir_ty::infer::reachable_typeck_diagnostics(&host, entry) + .iter() + .map(|diagnostic| diagnostic.lower(&host)), + ); + hir::diag::sort_dedup_rendered_diagnostics(&host, &mut diagnostics); + diagnostics + .into_iter() + .map(|diagnostic| diagnostic.message) + .collect() + } + + fn workspace_from_files(files: &[(&str, &str)], entry: &str) -> Workspace { + let mut workspace = Workspace::new(); + workspace.apply_file_changes(files.iter().map(|(path, contents)| { + WorkspaceFileChange::Set { + path: (*path).to_owned(), + contents: (*contents).to_owned(), + } + })); + workspace.set_entry(entry); + workspace + } + + #[test] + fn main_only_clean_program_has_driver_ordered_diagnostics() { + let source = "function main() returns (word) {\n return 1;\n}\n"; + let workspace = workspace_with_main(source); + + assert_eq!(messages(&workspace), driver_style_messages(source)); + assert!(workspace.diagnostics().is_empty()); + } + + #[test] + fn owned_diagnostics_preserve_heuristic_suggestion_applicability() { + let source = "function value() returns (word) { return 1; }\nfunction main() returns (word) { return vaue(); }\n"; + let workspace = workspace_with_main(source); + let diagnostic = workspace + .diagnostics() + .into_iter() + .find(|diagnostic| { + diagnostic.code.as_deref() + == Some(hir::diag::DiagnosticCode::NAMERES_UNDEFINED_NAME) + }) + .expect("undefined-name diagnostic"); + let suggestion = diagnostic + .suggestions + .first() + .expect("structured suggestion"); + let typo = source.find("vaue").expect("typo") as u32; + + assert_eq!(suggestion.title, "Replace with `value`"); + assert_eq!( + suggestion.applicability, + SuggestionApplicability::MaybeIncorrect + ); + assert_eq!( + suggestion.edits, + vec![DiagnosticTextEdit { + range: DiagRange { + file_url: "file:///main/main.solc".to_owned(), + start: typo, + end: typo + "vaue".len() as u32, + }, + replacement: "value".to_owned(), + }] + ); + } + + #[test] + fn owned_diagnostics_preserve_exact_suggestion_applicability() { + let source = "enum Option { None, Some(word) }\nfunction main(x: word) returns (Option) { return Some(x); }\n// migrate-syntax: keep-unqualified-constructor\n"; + let workspace = workspace_with_main(source); + let diagnostic = workspace + .diagnostics() + .into_iter() + .find(|diagnostic| { + diagnostic.code.as_deref() + == Some(hir::diag::DiagnosticCode::NAMERES_UNQUALIFIED_CONSTRUCTOR) + }) + .expect("unqualified-constructor diagnostic"); + let suggestion = diagnostic + .suggestions + .first() + .expect("structured suggestion"); + let constructor = source.rfind("Some").expect("constructor reference") as u32; + + assert_eq!(suggestion.title, "Replace with `Option.Some`"); + assert_eq!( + suggestion.applicability, + SuggestionApplicability::MachineApplicable + ); + assert_eq!( + suggestion.edits, + vec![DiagnosticTextEdit { + range: DiagRange { + file_url: "file:///main/main.solc".to_owned(), + start: constructor, + end: constructor + "Some".len() as u32, + }, + replacement: "Option.Some".to_owned(), + }] + ); + } + + #[test] + fn main_only_type_error_matches_lowered_driver_messages() { + let source = "function f() returns (word) {\n return true;\n}\n"; + let workspace = workspace_with_main(source); + let diagnostics = workspace.diagnostics(); + + assert_eq!(messages(&workspace), driver_style_messages(source)); + assert_eq!(diagnostics.len(), 1); + assert!( + diagnostics[0].message.contains("mismatched") + || diagnostics[0].message.contains("type") + ); + assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error); + } + + #[test] + fn main_only_name_resolution_error_matches_lowered_driver_messages() { + let source = "function addOne(x: word) returns (word) {\n return x + missingVar;\n}\n"; + let workspace = workspace_with_main(source); + let diagnostics = workspace.diagnostics(); + + assert_eq!(messages(&workspace), driver_style_messages(source)); + assert_eq!(diagnostics.len(), 1); + assert!(diagnostics[0].message.contains("missingVar")); + assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error); + assert!(!diagnostics[0].labels.is_empty()); + assert!(diagnostics[0].labels.iter().any(|label| label.is_primary)); + let primary = diagnostics[0].primary.as_ref().expect("primary range"); + assert!(primary.end > primary.start); + assert_eq!( + source + .get(primary.start as usize..primary.end as usize) + .expect("primary range is valid UTF-8 boundary"), + "missingVar" + ); + } + + #[test] + fn std_import_resolves_from_embedded_files() { + let workspace = workspace_with_main( + "import {addWord} from std;\n\nfunction main() returns (word) {\n return addWord(1, 2);\n}\n", + ); + + assert!(workspace.diagnostics().is_empty()); + assert!(workspace.entry_module().is_some()); + assert_eq!(messages(&workspace), raw_messages(&workspace)); + } + + #[test] + fn non_solcore_twin_never_replaces_or_unregisters_a_module() { + let mut workspace = Workspace::new(); + workspace.set_file( + "foo.solc", + "function value() returns (word) { return 1; }\nexport { value };\n".to_owned(), + ); + workspace.set_file( + "main.solc", + "import {value} from foo;\nfunction main() returns (word) { return value(); }\n" + .to_owned(), + ); + workspace.set_entry("main.solc"); + assert!(workspace.diagnostics().is_empty()); + + workspace.set_file("foo.txt", "not solcore source".to_owned()); + assert!(workspace.diagnostics().is_empty()); + + workspace.remove_file("foo.txt"); + assert!(workspace.diagnostics().is_empty()); + } + + #[test] + fn loading_reachable_module_invalidates_cached_not_loaded_import() { + let mut workspace = Workspace::new(); + workspace.set_file( + "main.solc", + "import {double} from math;\n\nfunction main() returns (word) {\n return double(21);\n}\n" + .to_owned(), + ); + workspace.set_file( + "math.solc", + "function double(x: word) returns (word) { return x; }\n\nexport { double };\n" + .to_owned(), + ); + workspace.set_entry("main.solc"); + + let math_key = workspace + .host + .module_key_for_virtual_path(&main_path("math.solc")) + .expect("math module key"); + assert!(workspace.host.module_files.remove(&math_key).is_some()); + workspace.host.sync_module_file_snapshot(); + let diagnostics = workspace.diagnostics(); + assert!( + diagnostics.iter().any(|diagnostic| { + diagnostic.code.as_deref() == Some(hir::diag::DiagnosticCode::MODULE_NOT_FOUND) + }), + "expected a module-not-found diagnostic before loading math, got {diagnostics:#?}" + ); + + let entry_key = workspace.entry_key().expect("entry key"); + load_reachable_modules(&mut workspace.host, entry_key); + + let diagnostics = workspace.diagnostics(); + assert!( + diagnostics.iter().all(|diagnostic| { + diagnostic.code.as_deref() != Some(hir::diag::DiagnosticCode::MODULE_NOT_FOUND) + && !diagnostic.message.contains("file not found") + }), + "expected no module-not-found diagnostic after loading math, got {diagnostics:#?}" + ); + } + + #[test] + fn incremental_file_updates_reanalyze_existing_source_file() { + let clean = "function main() returns (word) {\n return 1;\n}\n"; + let mut workspace = workspace_with_main(clean); + assert!(workspace.diagnostics().is_empty()); + + let before_file = workspace + .db() + .source_file(main_path("main.solc")) + .expect("main source file"); + workspace.set_file( + "main.solc", + "function addOne(x: word) returns (word) {\n return x + missingVar;\n}\n".to_owned(), + ); + let after_file = workspace + .db() + .source_file(main_path("main.solc")) + .expect("main source file"); + assert_eq!(before_file, after_file); + assert_eq!(workspace.diagnostics().len(), 1); + + workspace.set_file("main.solc", clean.to_owned()); + let restored_file = workspace + .db() + .source_file(main_path("main.solc")) + .expect("main source file"); + assert_eq!(before_file, restored_file); + assert!(workspace.diagnostics().is_empty()); + } + + #[test] + fn removed_virtual_file_is_revived_with_the_same_salsa_identity() { + let source = "function main() returns (word) { return 1; }\n"; + let mut host = AnalysisHost::new(); + let path = main_path("main.solc"); + let original = host.set_virtual_file(path.clone(), source.to_owned()); + let _ = parser::parse_file_to_hir(&host, original); + + host.remove_virtual_file(path.clone()); + assert!(host.source_file(&path).is_none()); + assert!(original.content(&host).is_none()); + + let revived = host.set_virtual_file(path.clone(), source.to_owned()); + assert_eq!(revived, original); + assert_eq!(revived.content(&host).as_deref(), Some(source)); + assert!(!host.tombstones.contains_key(&path)); + } + + #[test] + fn identical_virtual_and_workspace_updates_do_not_reexecute_queries() { + let source = "function main() returns (word) { return 1; }\n"; + let (mut host, executed) = host_with_execution_log(); + let file = host.set_virtual_file(main_path("main.solc"), source.to_owned()); + let _ = parser::parse_file_to_hir(&host, file); + let _ = take_executed(&executed); + + let same_file = host.set_virtual_file(main_path("main.solc"), source.to_owned()); + assert_eq!(same_file, file); + let _ = parser::parse_file_to_hir(&host, same_file); + let events = take_executed(&executed); + assert_eq!( + query_executions(&events, "parse_file_to_hir"), + 0, + "{events:#?}" + ); + + host.seed_std(); + let mut workspace = Workspace { + host, + entry_path: None, + }; + workspace.set_entry("main.solc"); + assert!(workspace.diagnostics().is_empty()); + let _ = take_executed(&executed); + + workspace.set_file("main.solc", source.to_owned()); + assert!(workspace.diagnostics().is_empty()); + let events = take_executed(&executed); + assert_eq!( + query_executions(&events, "parse_file_to_hir"), + 0, + "{events:#?}" + ); + } + + #[test] + fn incremental_diagnostics_match_a_fresh_workspace_across_batch_changes() { + let initial_main = + "import {value} from util;\nfunction main() returns (word) { return value(); }\n"; + let initial_util = "function value() returns (word) { return 1; }\nexport { value };\n"; + let mut incremental = workspace_from_files( + &[("main.solc", initial_main), ("util.solc", initial_util)], + "main.solc", + ); + assert!(incremental.diagnostics().is_empty()); + + let broken_main = "import {answer} from helper;\nfunction main() returns (word) { return answer(missing); }\n"; + let broken_helper = + "function answer(x: bool) returns (word) { return x; }\nexport { answer };\n"; + incremental.apply_file_changes([ + WorkspaceFileChange::Set { + path: "main.solc".to_owned(), + contents: broken_main.to_owned(), + }, + WorkspaceFileChange::Remove { + path: "util.solc".to_owned(), + }, + WorkspaceFileChange::Set { + path: "helper.solc".to_owned(), + contents: broken_helper.to_owned(), + }, + ]); + let fresh = workspace_from_files( + &[("main.solc", broken_main), ("helper.solc", broken_helper)], + "main.solc", + ); + assert_eq!(incremental.diagnostics(), fresh.diagnostics()); + + let fixed_main = "import {answer} from helper;\nfunction main() returns (word) { return answer(true); }\n"; + let fixed_helper = + "function answer(x: bool) returns (word) { return 1; }\nexport { answer };\n"; + incremental.apply_file_changes([ + WorkspaceFileChange::Set { + path: "main.solc".to_owned(), + contents: fixed_main.to_owned(), + }, + WorkspaceFileChange::Set { + path: "helper.solc".to_owned(), + contents: fixed_helper.to_owned(), + }, + ]); + let fresh = workspace_from_files( + &[("main.solc", fixed_main), ("helper.solc", fixed_helper)], + "main.solc", + ); + assert_eq!(incremental.diagnostics(), fresh.diagnostics()); + assert!(incremental.diagnostics().is_empty()); + } + + #[test] + fn batch_update_resolves_a_multi_hop_import_chain() { + let workspace = workspace_from_files( + &[ + ( + "main.solc", + "import {fromA} from a;\nfunction main() returns (word) { return fromA(); }\n", + ), + ( + "a.solc", + "import {value} from b;\nfunction fromA() returns (word) { return value(); }\nexport { fromA };\n", + ), + ( + "b.solc", + "function value() returns (word) { return 42; }\nexport { value };\n", + ), + ], + "main.solc", + ); + assert!(workspace.diagnostics().is_empty()); + } + + #[test] + fn embedded_std_file_set_is_exactly_the_expected_five_files() { + let names = STD_FILES + .iter() + .map(|(name, _)| *name) + .collect::>(); + assert_eq!( + names, + BTreeSet::from([ + "ABIGeneric.solc", + "Generic.solc", + "dispatch.solc", + "opcodes.solc", + "std.solc", + ]) + ); + assert!(STD_FILES.iter().all(|(_, contents)| !contents.is_empty())); + } + + #[test] + fn virtual_file_urls_encode_special_path_characters() { + let mut workspace = Workspace::new(); + workspace.set_file( + "nested/数 学#1.solc", + "function value() returns (word) { return 1; }\n".to_owned(), + ); + + let file = workspace + .db() + .source_file("/main/nested/数 学#1.solc") + .expect("virtual source file"); + + assert_eq!( + file.url(workspace.db()).as_str(), + "file:///main/nested/%E6%95%B0%20%E5%AD%A6%231.solc" + ); + } + + #[test] + #[ignore = "pathology workload run by scripts/check-compile-performance.sh"] + fn incremental_diagnostics_scaling_workload() { + fn source(revision: usize) -> String { + let mut source = String::new(); + for index in 0..256 { + source.push_str(&format!( + "function value{index}(x: word) returns (word) {{ return x; }}\n" + )); + } + source.push_str(&format!( + "function main() returns (word) {{ return value255({revision}); }}\n" + )); + source + } + + let mut workspace = workspace_with_main(&source(0)); + assert!(workspace.diagnostics().is_empty()); + for revision in 1..=64 { + workspace.set_file("main.solc", source(revision)); + assert!(workspace.diagnostics().is_empty(), "revision {revision}"); + } + } +} diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml new file mode 100644 index 00000000..096ea985 --- /dev/null +++ b/crates/wasm/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "solcore-wasm" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +serde_json = "1" +console_error_panic_hook = "0.1" +compiler = { workspace = true } +vfs = { workspace = true } +nameres = { workspace = true } +hull = { path = "../hull", package = "solcore-hull" } +yul = { path = "../yul", package = "solcore-yul" } +sonatina = { path = "../sonatina", package = "solcore-sonatina" } + +# wasm-pack's bundled wasm-opt is too old for reference-types (multiple tables). +# Size is handled by the workspace `[profile.wasm-release]` (strip + opt-level=z + lto); +# for a final size pass run a modern `wasm-opt -Oz --enable-reference-types` (see playground/README). +[package.metadata.wasm-pack.profile.custom] +wasm-opt = false diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs new file mode 100644 index 00000000..30db999b --- /dev/null +++ b/crates/wasm/src/lib.rs @@ -0,0 +1,774 @@ +//! Browser-facing `wasm-bindgen` API for compiling in-memory Solcore sources. + +use std::{collections::BTreeMap, path::Path}; + +use nameres::Db as _; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use vfs::{ + AnalysisHost, DiagRange, DiagnosticSeverity, MAIN_ROOT, STD_FILES, STD_ROOT, Workspace, + WorkspaceFileChange, +}; +use wasm_bindgen::prelude::*; + +/// Installs a panic hook so browser console errors include Rust panic details. +#[wasm_bindgen(start)] +pub fn __start() { + console_error_panic_hook::set_once(); +} + +/// Compile a virtual workspace and return diagnostics plus requested outputs. +/// +/// `input` is a JS object: +/// `{ files: [{ path: string, content: string }], entry: string, +/// options?: { emitHull?: bool, emitYul?: bool, emitSonatina?: bool, +/// emitAbi?: bool } }`. +#[wasm_bindgen] +pub fn compile(input: JsValue) -> Result { + let input = serde_wasm_bindgen::from_value(input) + .map_err(|err| JsValue::from_str(&format!("invalid compile input: {err}")))?; + let result = compile_impl(input); + result + .serialize(&serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true)) + .map_err(|err| JsValue::from_str(&format!("failed to serialize compile result: {err}"))) +} + +/// Returns the embedded standard library files as `{ path, content }` objects. +#[wasm_bindgen] +pub fn std_files() -> JsValue { + let files = STD_FILES + .iter() + .map(|(path, content)| FileOutput { + path: (*path).to_owned(), + content: (*content).to_owned(), + }) + .collect::>(); + match serde_wasm_bindgen::to_value(&files) { + Ok(value) => value, + Err(_) => JsValue::NULL, + } +} + +/// Returns the compiler package version for UI display. +#[wasm_bindgen] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_owned() +} + +#[derive(Deserialize)] +pub(crate) struct CompileInput { + pub(crate) files: Vec, + pub(crate) entry: String, + #[serde(default)] + pub(crate) options: Options, +} + +#[derive(Deserialize)] +pub(crate) struct FileInput { + pub(crate) path: String, + pub(crate) content: String, +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Options { + #[serde(default)] + pub(crate) emit_hull: bool, + #[serde(default)] + pub(crate) emit_yul: bool, + #[serde(default)] + pub(crate) emit_sonatina: bool, + #[serde(default)] + pub(crate) emit_abi: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CompileResult { + pub(crate) success: bool, + pub(crate) diagnostics: Vec, + pub(crate) hull: Option, + pub(crate) yul: Option, + pub(crate) sonatina: Option, + pub(crate) abi: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Diag { + pub(crate) severity: String, + pub(crate) code: Option, + pub(crate) message: String, + pub(crate) primary: Option, + pub(crate) labels: Vec