From 9ad87f759153acf71dd9e74af4f38da9a3c0a99f Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Wed, 29 Jul 2026 23:37:55 +0400 Subject: [PATCH 01/11] ci: switch main-slang to the slang pipeline (#617) --- .github/actions/build-rust/action.yml | 19 +- .github/actions/build-toolchain/action.yml | 7 +- .github/actions/rust-unit-tests/action.yml | 2 +- .github/workflows/cache-warmup.yaml | 9 +- .github/workflows/compile-benchmark.yaml | 36 +--- .github/workflows/coverage.yaml | 4 +- .github/workflows/integration-tests.yaml | 113 ++++++----- .github/workflows/release.yaml | 28 ++- .github/workflows/sanitizer.yaml | 12 +- .github/workflows/slang-tests.yaml | 207 --------------------- .github/workflows/test.yaml | 117 ++++++++---- .gitmodules | 2 +- solx-dev/src/test/mod.rs | 2 +- solx-solidity | 2 +- 14 files changed, 192 insertions(+), 368 deletions(-) delete mode 100644 .github/workflows/slang-tests.yaml diff --git a/.github/actions/build-rust/action.yml b/.github/actions/build-rust/action.yml index acc2b0358..6e62447ce 100644 --- a/.github/actions/build-rust/action.yml +++ b/.github/actions/build-rust/action.yml @@ -1,9 +1,6 @@ name: 'Build' description: 'Build Rust executable.' inputs: - exec_name: - description: 'Name of the executable.' - required: true target: description: 'Specific build target triplet.' required: false @@ -82,25 +79,27 @@ runs: if [[ "${RUNNER_OS}" == "Windows" ]] || [[ "${RUNNER_OS}" == "Linux" ]]; then export RUSTFLAGS="${RUSTFLAGS} -C link-arg=-fuse-ld=lld" fi - ${SFW_PREFIX:-} cargo ${BUILD_STD_LIB} build --frozen ${RELEASE} ${{ steps.build-target.outputs.target }} + ${SFW_PREFIX:-} cargo ${BUILD_STD_LIB} build-slang --frozen ${RELEASE} ${{ steps.build-target.outputs.target }} if [ '${{ inputs.target }}' != '' ]; then - BINARY_DIR="${PWD}/target/${{ inputs.target }}/${{ inputs.build-type }}" + BINARY_DIR="${PWD}/target-slang/${{ inputs.target }}/${{ inputs.build-type }}" else - BINARY_DIR="${PWD}/target/${{ inputs.build-type }}" + BINARY_DIR="${PWD}/target-slang/${{ inputs.build-type }}" fi [ "$RUNNER_OS" = "Windows" ] && WIN_SUFFIX=".exe" echo "${BINARY_DIR}" >> "${GITHUB_PATH}" - echo "binary-path=${BINARY_DIR}/${{ inputs.exec_name }}${WIN_SUFFIX}" >> "${GITHUB_OUTPUT}" + echo "binary-path=${BINARY_DIR}/solx${WIN_SUFFIX}" >> "${GITHUB_OUTPUT}" - name: Prepare binary if: inputs.release-suffix != '' + env: + BINARY_PATH: ${{ steps.build.outputs.binary-path }} shell: ${{ runner.os == 'Windows' && 'msys2 {0}' || 'bash' }} run: | mkdir -p ./releases/${{ inputs.release-suffix }} [ "$RUNNER_OS" = "Windows" ] && WIN_SUFFIX=".exe" - strip "./target/${{ inputs.target }}/${{ inputs.build-type }}/${{ inputs.exec_name }}${WIN_SUFFIX}" - mv "./target/${{ inputs.target }}/${{ inputs.build-type }}/${{ inputs.exec_name }}${WIN_SUFFIX}" \ - "./releases/${{ inputs.release-suffix }}/${{ inputs.exec_name }}-${{ inputs.release-suffix }}${WIN_SUFFIX}" + strip "${BINARY_PATH}" + mv "${BINARY_PATH}" \ + "./releases/${{ inputs.release-suffix }}/solx-${{ inputs.release-suffix }}${WIN_SUFFIX}" - name: Upload binary if: inputs.release-suffix != '' diff --git a/.github/actions/build-toolchain/action.yml b/.github/actions/build-toolchain/action.yml index d0825d0c5..e46129cea 100644 --- a/.github/actions/build-toolchain/action.yml +++ b/.github/actions/build-toolchain/action.yml @@ -3,9 +3,9 @@ description: >- Shared CI prologue for the standard toolchain configuration: submodule checkout, Socket Firewall, platform prep (MSYS2 on Windows, disk cleanup on macOS), then the LLVM and solc builds. The build parameters below - define the standard-config cache keys shared by test.yaml, - slang-tests.yaml, and cache-warmup.yaml — changing them here re-keys all - of those workflows together. Workflows needing a different LLVM + define the standard-config cache keys shared by test.yaml and + cache-warmup.yaml — changing them here re-keys both workflows together. + Workflows needing a different LLVM configuration (sanitizer, coverage, integration, release) call build-llvm/build-solc directly and pair with their own cache-warmup job. @@ -79,3 +79,4 @@ runs: with: cmake-build-type: RelWithDebInfo working-dir: 'solx-solidity' + enable-mlir: 'true' diff --git a/.github/actions/rust-unit-tests/action.yml b/.github/actions/rust-unit-tests/action.yml index 978c7ede0..5ad39885c 100644 --- a/.github/actions/rust-unit-tests/action.yml +++ b/.github/actions/rust-unit-tests/action.yml @@ -131,7 +131,7 @@ runs: export "CARGO_TARGET_${TARGET_UPPER}_LINKER=clang" fi if [ '${{ inputs.target }}' != '' ]; then - export CARGO_BIN_EXE_SOLX="./target/${{ inputs.target }}/debug/solx" + export CARGO_BIN_EXE_SOLX="./${CARGO_TARGET_DIR:-target}/${{ inputs.target }}/debug/solx" fi set -o pipefail if [ "${{ inputs.cargo-test-command }}" = "test" ]; then ${SFW_PREFIX:-} cargo build --frozen ${{ steps.build-target.outputs.target }}; fi diff --git a/.github/workflows/cache-warmup.yaml b/.github/workflows/cache-warmup.yaml index fbff6e365..17b5b77d7 100644 --- a/.github/workflows/cache-warmup.yaml +++ b/.github/workflows/cache-warmup.yaml @@ -87,7 +87,7 @@ jobs: with: path: ${{ runner.temp }}/ccache-llvm key: ccache-touch-${{ github.job }}-${{ github.run_id }} - restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-RelWithDebInfo-mlir-end + restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-RelWithDebInfo-mlir-utils-end fail-on-cache-miss: false lookup-only: true @@ -176,6 +176,7 @@ jobs: with: build-type: RelWithDebInfo enable-assertions: 'true' + enable-mlir: 'true' sanitizer: 'Address' - name: Touch LLVM ccache @@ -184,7 +185,7 @@ jobs: with: path: ${{ runner.temp }}/ccache-llvm key: ccache-touch-${{ github.job }}-${{ github.run_id }} - restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-RelWithDebInfo-Address-end + restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-RelWithDebInfo-mlir-Address-end fail-on-cache-miss: false lookup-only: true @@ -265,6 +266,7 @@ jobs: with: build-type: Release enable-assertions: 'false' + enable-mlir: 'true' - name: Touch LLVM ccache if: always() @@ -272,7 +274,7 @@ jobs: with: path: ${{ runner.temp }}/ccache-llvm key: ccache-touch-${{ github.job }}-${{ github.run_id }} - restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-no-assertions-end + restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-mlir-no-assertions-end fail-on-cache-miss: false lookup-only: true @@ -281,6 +283,7 @@ jobs: with: cmake-build-type: Release working-dir: 'solx-solidity' + enable-mlir: 'true' - name: Touch solc ccache if: always() diff --git a/.github/workflows/compile-benchmark.yaml b/.github/workflows/compile-benchmark.yaml index 2a14dfe4e..6634f66d2 100644 --- a/.github/workflows/compile-benchmark.yaml +++ b/.github/workflows/compile-benchmark.yaml @@ -1,7 +1,7 @@ name: Compile Benchmark # Times `solx --standard-json` over the fixtures in tests/benchmark/fixtures -# with hyperfine, comparing the PR build against a main build and the latest +# with hyperfine, comparing the PR build against a main-slang build and the latest # release binary, and posts the report as a PR comment. Label-gated like the # integration suite; report-only (no pass/fail gating on timings). @@ -69,14 +69,14 @@ jobs: # actions/checkout's `git clean -ffdx` doesn't recurse into submodules, # so on a persistent self-hosted workspace a previous run can leave # stale dirs inside temp-solx-main. Wipe wholesale before re-checking - # out main. (Same pattern as integration-tests.yaml.) + # out main-slang. (Same pattern as integration-tests.yaml.) - name: Reset main checkout dir run: rm -rf temp-solx-main - - name: Checkout main + - name: Checkout main-slang uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - ref: main + ref: main-slang submodules: true persist-credentials: false path: temp-solx-main @@ -102,6 +102,7 @@ jobs: with: build-type: Release enable-assertions: 'false' + enable-mlir: 'true' - name: Build LLVM (main) uses: ./.github/actions/build-llvm @@ -110,44 +111,27 @@ jobs: mirror-key: ${{ steps.build-llvm-pr.outputs.cache-key }} build-type: Release enable-assertions: 'false' - - - name: Build solc (PR) - id: build-solc-pr - uses: ./.github/actions/build-solc - with: - cmake-build-type: Release - working-dir: 'solx-solidity' - - - name: Build solc (main) - uses: ./.github/actions/build-solc - with: - cmake-build-type: 'Release' - working-dir: 'temp-solx-main/solx-solidity' - mirror-key: ${{ steps.build-solc-pr.outputs.cache-key }} + enable-mlir: 'true' # Env and target mirror .github/actions/build-rust so the pr/main # binaries share the released binary's build configuration: the # statically linked glibc alone is worth ~5% on this workload. - name: Build solx (PR) env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build RUSTC_BOOTSTRAP: 1 RUSTFLAGS: '-C target-feature=+crt-static -C link-arg=-fuse-ld=lld' run: | ${SFW_PREFIX:-} cargo fetch --locked - ${SFW_PREFIX:-} cargo build --frozen --release --bin solx --target x86_64-unknown-linux-gnu + ${SFW_PREFIX:-} cargo build-slang --frozen --release --target x86_64-unknown-linux-gnu - name: Build solx (main) working-directory: temp-solx-main env: - BOOST_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/build RUSTC_BOOTSTRAP: 1 RUSTFLAGS: '-C target-feature=+crt-static -C link-arg=-fuse-ld=lld' run: | ${SFW_PREFIX:-} cargo fetch --locked - ${SFW_PREFIX:-} cargo build --frozen --release --bin solx --target x86_64-unknown-linux-gnu + ${SFW_PREFIX:-} cargo build-slang --frozen --release --target x86_64-unknown-linux-gnu - name: Download latest release binary env: @@ -180,8 +164,8 @@ jobs: - name: Run benchmark run: | ./tests/benchmark/run.sh \ - --bin pr=./target/x86_64-unknown-linux-gnu/release/solx \ - --bin main=./temp-solx-main/target/x86_64-unknown-linux-gnu/release/solx \ + --bin pr=./target-slang/x86_64-unknown-linux-gnu/release/solx \ + --bin main=./temp-solx-main/target-slang/x86_64-unknown-linux-gnu/release/solx \ --bin release=./solx-release \ --runs 3 --warmup 1 \ --out benchmark-out diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 068727067..c3a13efe8 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -2,6 +2,8 @@ name: Code coverage on: pull_request: + branches: + - main # Main runs upload the Codecov base for PR deltas and save the cargo cache # that PR runs restore (rust-cache save-if only fires on refs/heads/main). push: @@ -98,12 +100,12 @@ jobs: with: cmake-build-type: RelWithDebInfo working-dir: 'solx-solidity' + enable-mlir: 'true' - name: Build solx id: build-solx uses: ./.github/actions/build-rust with: - exec_name: 'solx' build-type: 'release' enable-coverage: 'true' target: 'x86_64-unknown-linux-gnu' diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 1847c4561..9a2246051 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -62,17 +62,16 @@ jobs: # actions/checkout's `git clean -ffdx` doesn't recurse into submodules, # so on a persistent self-hosted workspace a previous run can leave - # stale solx-solidity/{build,boost} dirs (or symlinks pointing at the - # PR's freshly-built artifacts) inside temp-solx-main. Wipe wholesale - # before re-checking out main. - - name: Reset main checkout dir + # stale artifacts inside temp-solx-main. Wipe wholesale before + # re-checking out main-slang. + - name: Reset main-slang checkout dir run: rm -rf temp-solx-main - - name: Checkout main + - name: Checkout main-slang uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 continue-on-error: true with: - ref: main + ref: main-slang submodules: true persist-credentials: false path: temp-solx-main @@ -80,7 +79,7 @@ jobs: - name: Checkout submodules (PR) uses: ./.github/actions/checkout-submodules - - name: Checkout submodules (main) + - name: Checkout submodules (main-slang) uses: ./.github/actions/checkout-submodules continue-on-error: true with: @@ -97,73 +96,79 @@ jobs: with: build-type: Release enable-assertions: 'false' - - - name: Build LLVM (main) - uses: ./.github/actions/build-llvm + enable-mlir: 'true' + + # Submodule SHA alone is not sufficient: `solx-dev llvm build` is + # driven by `solx-dev/src/llvm/*`, so a PR that edits those scripts + # without bumping the solx-llvm submodule would silently share its own + # LLVM as the main-slang baseline. + - name: Share LLVM with main-slang checkout + id: share-llvm continue-on-error: true - with: - working-dir: temp-solx-main - mirror-key: ${{ steps.build-llvm-pr.outputs.cache-key }} - build-type: Release - enable-assertions: 'false' - - - name: Build solc (PR) - id: build-solc-pr - uses: ./.github/actions/build-solc - with: - cmake-build-type: Release - working-dir: 'solx-solidity' + run: | + # Content hash of a directory tree (order-independent). + hash_tree() { + find "$1" -type f -print0 | sort -z | xargs -0 cat | sha256sum | cut -d' ' -f1 + } + MAIN_SHA=$(git -C temp-solx-main/solx-llvm rev-parse HEAD) + PR_SHA=$(git -C solx-llvm rev-parse HEAD) + SCRIPT_PR=$(hash_tree solx-dev/src/llvm) + SCRIPT_MAIN=$(hash_tree temp-solx-main/solx-dev/src/llvm) + if [ "$MAIN_SHA" = "$PR_SHA" ] && [ "$SCRIPT_MAIN" = "$SCRIPT_PR" ]; then + echo "LLVM SHAs and build scripts match ($PR_SHA / ${SCRIPT_PR:0:8}), sharing via symlink" + mkdir -p temp-solx-main/target-llvm + rm -rf temp-solx-main/target-llvm/target-final + ln -snf "$GITHUB_WORKSPACE/target-llvm/target-final" \ + temp-solx-main/target-llvm/target-final + echo "shared=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::LLVM inputs differ (main=$MAIN_SHA/${SCRIPT_MAIN:0:8} PR=$PR_SHA/${SCRIPT_PR:0:8}), building main separately" + fi - - name: Build solc (main) - uses: ./.github/actions/build-solc + # If share-llvm failed entirely (output empty), fall through and build. + - name: Build LLVM (main-slang) + if: steps.share-llvm.outputs.shared != 'true' continue-on-error: true - with: - cmake-build-type: 'Release' - working-dir: 'temp-solx-main/solx-solidity' - mirror-key: ${{ steps.build-solc-pr.outputs.cache-key }} + run: | + pushd temp-solx-main + ${SFW_PREFIX:-} cargo fetch --locked + ${SFW_PREFIX:-} cargo build --frozen --release --bin solx-dev + ./target/release/solx-dev llvm build --build-type Release --enable-mlir --install-distribution + popd - name: Fetch dependencies (PR) run: ${SFW_PREFIX:-} cargo fetch --locked - name: Build solx (PR) - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build - run: ${SFW_PREFIX:-} cargo build --frozen --release --bin solx + run: | + ${SFW_PREFIX:-} cargo build-slang --frozen --release + mkdir -p target/release + cp target-slang/release/solx target/release/solx - name: Build solx-dev and solx-tester (PR) - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build - run: ${SFW_PREFIX:-} cargo build --frozen --release --bin solx-dev --bin solx-tester + run: ${SFW_PREFIX:-} cargo build --frozen --release -p solx-dev -p solx-tester --features solx-tester/slang-ast - - name: Fetch dependencies (main) + - name: Fetch dependencies (main-slang) working-directory: temp-solx-main continue-on-error: true run: ${SFW_PREFIX:-} cargo fetch --locked - - name: Build solx (main) + - name: Build solx (main-slang) working-directory: temp-solx-main continue-on-error: true - env: - BOOST_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/build - run: ${SFW_PREFIX:-} cargo build --frozen --release --bin solx + run: | + ${SFW_PREFIX:-} cargo build-slang --frozen --release + mkdir -p target/release + cp target-slang/release/solx target/release/solx - - name: Build solx-dev and solx-tester (main) + - name: Build solx-dev and solx-tester (main-slang) working-directory: temp-solx-main continue-on-error: true - env: - BOOST_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/build - run: ${SFW_PREFIX:-} cargo build --frozen --release --bin solx-dev --bin solx-tester + run: ${SFW_PREFIX:-} cargo build --frozen --release -p solx-dev -p solx-tester --features solx-tester/slang-ast - name: Run solx-tester tests (PR) id: solx_tester_tests continue-on-error: true - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build run: ./target/release/solx-dev test solx-tester --solidity-compiler ./target/release/solx # When `ci:integration-benchmark-full` is present, enable every @@ -184,13 +189,10 @@ jobs: ' "$cfg" > "$cfg.tmp" && mv "$cfg.tmp" "$cfg" done - - name: Run solx-tester benchmarks (main) + - name: Run solx-tester benchmarks (main-slang) id: solx_tester_bench_main continue-on-error: true working-directory: temp-solx-main - env: - BOOST_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/temp-solx-main/solx-solidity/build run: | ./target/release/solx-dev test solx-tester \ --solidity-compiler ./target/release/solx \ @@ -200,9 +202,6 @@ jobs: - name: Run solx-tester benchmarks (PR) id: solx_tester_bench_pr continue-on-error: true - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build run: | ./target/release/solx-dev test solx-tester \ --solidity-compiler ./target/release/solx \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 844c3186f..c89ea8892 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,7 +7,7 @@ # # workflow_dispatch — Nightly / ad-hoc pre-release: same build & review # pipeline, publishes a GitHub pre-release. Individual -# platforms can be toggled off. Requires manual approval. +# platforms can be toggled off. No approval needed. # # pull_request (ci:release label) — Dry-run: exercises the full pipeline but # skips attestation, GitHub Release creation, docs @@ -179,13 +179,7 @@ jobs: with: build-type: Release enable-assertions: 'false' - enable-mlir: 'false' - - - name: Building solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: 'Release' - working-dir: 'solx-solidity' + enable-mlir: 'true' - name: Free disk space (remove LLVM build artifacts) shell: bash @@ -194,13 +188,17 @@ jobs: rm -rf target-llvm/build-final echo "After cleanup:" && df -h . + # bindgen's embedded clang needs to know it's targeting MinGW, + # otherwise it can't parse MinGW-specific __attribute__ extensions + # in stdlib.h (rust-lang/rust-bindgen#1760). + - name: Set bindgen target for MinGW + if: runner.os == 'Windows' + shell: bash + run: echo "BINDGEN_EXTRA_CLANG_ARGS=--target=x86_64-w64-windows-gnu" >> "${GITHUB_ENV}" + - name: Build solx uses: ./.github/actions/build-rust - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build with: - exec_name: 'solx' target: ${{ matrix.target }} release-suffix: ${{ format('{0}-{1}', matrix.release-suffix, github.ref_type == 'tag' && format('v{0}', github.ref_name) || inputs.prerelease_suffix || 'notag') }} @@ -486,8 +484,8 @@ jobs: && needs.prepare.result == 'success' && needs.review.result == 'success' && needs.cooldown-check.result == 'success' - # PR dry-runs skip the manual approval gate; tag/dispatch runs still require it. - environment: ${{ github.event_name != 'pull_request' && 'solx-release' || '' }} + # Only tag releases require the manual approval gate; pre-releases publish unattended. + environment: ${{ github.ref_type == 'tag' && 'solx-release' || '' }} runs-on: ubuntu-24.04 steps: @@ -658,7 +656,7 @@ jobs: RELEASE_TITLE: ${{ needs.prepare.outputs.release_title }} VERSION_OR_SHA: ${{ needs.prepare.outputs.version_or_sha }} IS_PRERELEASE: ${{ github.ref_type != 'tag' }} - ENVIRONMENT: ${{ github.event_name != 'pull_request' && 'solx-release' || 'none (dry-run)' }} + ENVIRONMENT: ${{ github.ref_type == 'tag' && 'solx-release' || 'none' }} run: | echo "### Release Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/sanitizer.yaml b/.github/workflows/sanitizer.yaml index c828a66e7..10f0fd3dd 100644 --- a/.github/workflows/sanitizer.yaml +++ b/.github/workflows/sanitizer.yaml @@ -70,6 +70,7 @@ jobs: TARGET: x86_64-unknown-linux-gnu RUST_SANITIZER: 'address' LLVM_SANITIZER: 'Address' + CARGO_TARGET_DIR: target-slang steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -91,21 +92,14 @@ jobs: build-type: RelWithDebInfo sanitizer: ${{ env.LLVM_SANITIZER }} enable-assertions: 'true' - - - name: Building solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: RelWithDebInfo - working-dir: 'solx-solidity' + enable-mlir: 'true' - name: Run tests uses: ./.github/actions/rust-unit-tests - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build with: target: ${{ env.TARGET }} sanitizer: ${{ env.RUST_SANITIZER }} + cargo-test-command: test-slang pr-checks: name: PR Checks (Sanitizer) diff --git a/.github/workflows/slang-tests.yaml b/.github/workflows/slang-tests.yaml deleted file mode 100644 index ff04838d5..000000000 --- a/.github/workflows/slang-tests.yaml +++ /dev/null @@ -1,207 +0,0 @@ -name: Slang Tests - -on: - pull_request: - types: [opened, synchronize, reopened, labeled] - merge_group: - push: - branches: - - main - -permissions: - contents: read - packages: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - - label-check: - # Checks the PR's current label set, not the triggering action: keying on - # github.event.label.name would skip this run (and cancel a real one in - # flight) when an unrelated label is added to a PR carrying ci:slang. - if: >- - github.event_name == 'push' - || (contains(github.event.pull_request.labels.*.name, 'ci:slang') - && !github.event.pull_request.head.repo.fork) - runs-on: ubuntu-24.04 - steps: - - run: 'true' - - changes: - needs: label-check - runs-on: ubuntu-24.04 - permissions: - contents: read - outputs: - code: ${{ steps.filter.outputs.code }} - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - # Full history for the filter's git-diff mode. - fetch-depth: 0 - - uses: ./.github/actions/code-changes - id: filter - - cooldown-check: - name: Cargo cooldown check - runs-on: ubuntu-24.04 - needs: [label-check, changes] - # A custom if: overrides the implicit skip cascade from needs - # (actions/runner#491), so the label gate must be restated here. - if: needs.label-check.result == 'success' && needs.changes.outputs.code == 'true' - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - uses: ./.github/actions/cooldown-check - - slang-tests: - needs: [label-check, cooldown-check] - # checks: write for the rust-unit-tests action's test-results check run. - permissions: - contents: read - checks: write - packages: read - env: - CARGO_TARGET_DIR: target-slang - CARGO_INCREMENTAL: "0" - strategy: - fail-fast: false - matrix: - include: - - name: "MacOS x86" - runner: macos-15-intel - - name: "MacOS arm64" - runner: macos-15 - - name: "Linux x86 gnu" - runner: ubuntu-24.04 - image: ghcr.io/nomicfoundation/solx-ci-runner@sha256:cd5a37f2630fdf1898ddb2ca11f8c2ecc4d572c60011fe923b27b1625f92ba15 - target: "x86_64-unknown-linux-gnu" - - name: "Linux ARM64 gnu" - runner: ubuntu-24.04-arm - image: ghcr.io/nomicfoundation/solx-ci-runner@sha256:cd5a37f2630fdf1898ddb2ca11f8c2ecc4d572c60011fe923b27b1625f92ba15 - target: "aarch64-unknown-linux-gnu" - - name: "Windows" - runner: windows-2025 - target: "x86_64-pc-windows-gnu" - runs-on: ${{ matrix.runner }} - container: - image: ${{ matrix.image || '' }} - name: ${{ matrix.name }} - steps: - - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - submodules: true - persist-credentials: false - - # solc is built separately below: the lit tests need a Release build - # against a synced solx-solidity, not the standard config. - - name: Build toolchain - uses: ./.github/actions/build-toolchain - with: - solc: 'false' - - # TODO: remove this step and the Build solc step below once the lit - # tests stop running against solc. - - name: Sync solx-solidity for MLIR codegen - shell: bash - run: | - git -C solx-solidity fetch origin main - git -C solx-solidity checkout FETCH_HEAD - - # Release: RelWithDebInfo bakes -g3 debug info into the statically - # linked solc.exe, producing a ~2 GB PE that the Windows loader - # rejects ("Exec format error"), and the lit tests execute solc. - - name: Build solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: Release - enable-mlir: 'true' - - # bindgen's embedded clang needs to know it's targeting MinGW, - # otherwise it can't parse MinGW-specific __attribute__ extensions - # in stdlib.h (rust-lang/rust-bindgen#1760). - - name: Set bindgen target for MinGW - if: runner.os == 'Windows' - shell: bash - run: echo "BINDGEN_EXTRA_CLANG_ARGS=--target=x86_64-w64-windows-gnu" >> "${GITHUB_ENV}" - - - name: Preserve lit tools before disk cleanup - shell: bash - run: | - EXE=${{ runner.os == 'Windows' && '.exe' || '' }} - LIT=llvm-lit${{ runner.os == 'Windows' && '.py' || '' }} - mkdir -p "${RUNNER_TEMP}/lit-tools" - cp -r \ - target-llvm/target-final/bin/FileCheck${EXE} \ - target-llvm/target-final/bin/${LIT} \ - solx-llvm/llvm/utils/lit \ - solx-solidity/build/solc/solc${EXE} \ - "${RUNNER_TEMP}/lit-tools/" - echo "${RUNNER_TEMP}/lit-tools" >> "${GITHUB_PATH}" - echo "PYTHONPATH=${RUNNER_TEMP}/lit-tools/lit" >> "${GITHUB_ENV}" - - - name: Free disk space (remove build artifacts before slang tests) - shell: bash - run: | - echo "Before cleanup:" && df -h . - rm -rf solx-llvm target-llvm/build-final target solx-solidity - echo "After cleanup:" && df -h . - - - name: Cache cargo artifacts - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - prefix-key: slang-tests-v2 - # `cargo test-slang` writes to target-slang, not target (.cargo/config.toml alias) - workspaces: ". -> target-slang" - cache-on-failure: true - save-if: ${{ github.ref == 'refs/heads/main' }} - - - name: Run slang tests - uses: ./.github/actions/rust-unit-tests - with: - target: ${{ matrix.target || '' }} - cargo-test-command: 'test-slang' - results-xml: 'slang-unit-tests-results.xml' - check-name: 'Slang Unit Tests Results' - - - name: Verify lit tools run outside MSYS2 - if: runner.os == 'Windows' - shell: bash - run: | - ls -la "${RUNNER_TEMP}/lit-tools" - FileCheck --version - solc --version - - - name: Run MLIR lit tests - shell: bash - env: - SOLX_LIT_TARGET: ${{ matrix.target || '' }} - run: | - LIT=llvm-lit${{ runner.os == 'Windows' && '.py' || '' }} - ${LIT} -v solx-mlir/tests/lit/ - - pr-checks: - name: PR Checks (Slang) - runs-on: ubuntu-24.04 - if: always() - needs: - - label-check - - changes - - cooldown-check - - slang-tests - steps: - - name: Decide on PR checks - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1 - with: - jobs: ${{ toJSON(needs) }} - allowed-skips: >- - ${{ needs.label-check.result == 'skipped' && 'label-check, changes, cooldown-check, slang-tests' - || needs.changes.outputs.code != 'true' && 'cooldown-check, slang-tests' - || '' }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index eef9aad34..f458125d7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,19 +1,17 @@ name: Tests -# Execute workflow for each PR and with each merge to the trunk on: pull_request: types: [opened, synchronize, reopened] merge_group: push: branches: - - main + - main-slang permissions: contents: read packages: read -# Cancel the workflow if any new changes pushed to a feature branch or the trunk concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -133,7 +131,7 @@ jobs: with: prefix-key: cargo-checks-v1 cache-on-failure: true - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main-slang' }} - name: Cargo checks uses: ./.github/actions/cargo-check @@ -142,7 +140,9 @@ jobs: name: Cargo cooldown check runs-on: ubuntu-24.04 needs: changes - if: needs.changes.outputs.code == 'true' + if: >- + needs.changes.outputs.code == 'true' + && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -178,18 +178,18 @@ jobs: LOG_LEVEL: debug run: npx --yes renovate --platform=local --dry-run=full - # Build and run regression tests build-and-test: - needs: changes - if: needs.changes.outputs.code == 'true' || needs.changes.outputs.docs-examples == 'true' - env: - CARGO_INCREMENTAL: "0" + needs: [changes, cooldown-check] + # checks: write for the rust-unit-tests action's test-results check run. permissions: contents: read checks: write packages: read + env: + CARGO_TARGET_DIR: target-slang + CARGO_INCREMENTAL: "0" strategy: - fail-fast: false # finalize testing of all targets even if one failed + fail-fast: false matrix: include: - name: "MacOS x86" @@ -230,36 +230,86 @@ jobs: if: runner.os == 'Linux' && matrix.image != '' uses: ./.github/actions/free-disk-space-linux + # solc is built separately below: the lit tests need a Release build, + # not the standard config. - name: Build toolchain uses: ./.github/actions/build-toolchain + with: + solc: 'false' - # Positioned after build-toolchain: prepare-msys switches the rustup - # host, which is part of the cache key, so only the post-msys position - # matches the keys already saved on main. - - name: Cache cargo artifacts - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + # TODO: remove this step once the lit tests stop running against solc. + # + # Release: RelWithDebInfo bakes -g3 debug info into the statically + # linked solc.exe, producing a ~2 GB PE that the Windows loader + # rejects ("Exec format error"), and the lit tests execute solc. + - name: Build solc + uses: ./.github/actions/build-solc with: - prefix-key: build-and-test-v2 - cache-on-failure: true - save-if: ${{ github.ref == 'refs/heads/main' }} + cmake-build-type: Release + enable-mlir: 'true' - - name: Free disk space (remove LLVM source and build artifacts) + # bindgen's embedded clang needs to know it's targeting MinGW, + # otherwise it can't parse MinGW-specific __attribute__ extensions + # in stdlib.h (rust-lang/rust-bindgen#1760). + - name: Set bindgen target for MinGW + if: runner.os == 'Windows' + shell: bash + run: echo "BINDGEN_EXTRA_CLANG_ARGS=--target=x86_64-w64-windows-gnu" >> "${GITHUB_ENV}" + + - name: Preserve lit tools before disk cleanup + shell: bash + run: | + EXE=${{ runner.os == 'Windows' && '.exe' || '' }} + LIT=llvm-lit${{ runner.os == 'Windows' && '.py' || '' }} + mkdir -p "${RUNNER_TEMP}/lit-tools" + cp -r \ + target-llvm/target-final/bin/FileCheck${EXE} \ + target-llvm/target-final/bin/${LIT} \ + solx-llvm/llvm/utils/lit \ + solx-solidity/build/solc/solc${EXE} \ + "${RUNNER_TEMP}/lit-tools/" + echo "${RUNNER_TEMP}/lit-tools" >> "${GITHUB_PATH}" + echo "PYTHONPATH=${RUNNER_TEMP}/lit-tools/lit" >> "${GITHUB_ENV}" + + - name: Free disk space (remove build artifacts before slang tests) shell: bash run: | echo "Before cleanup:" && df -h . - rm -rf solx-llvm target-llvm/build-final - # Remove solx-solidity source, keeping build/ and boost/ for tests - find solx-solidity -mindepth 1 -maxdepth 1 ! -name build ! -name boost -exec rm -rf {} + - rm -rf .git/modules + rm -rf solx-llvm target-llvm/build-final target solx-solidity echo "After cleanup:" && df -h . + - name: Cache cargo artifacts + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + prefix-key: slang-tests-v2 + # `cargo test-slang` writes to target-slang, not target (.cargo/config.toml alias) + workspaces: ". -> target-slang" + cache-on-failure: true + save-if: ${{ github.ref == 'refs/heads/main-slang' }} + - name: Run tests uses: ./.github/actions/rust-unit-tests - env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build with: target: ${{ matrix.target || '' }} + cargo-test-command: 'test-slang' + results-xml: 'unit-tests-results.xml' + check-name: 'Unit Tests Results' + + - name: Verify lit tools run outside MSYS2 + if: runner.os == 'Windows' + shell: bash + run: | + ls -la "${RUNNER_TEMP}/lit-tools" + FileCheck --version + solc --version + + - name: Run MLIR lit tests + shell: bash + env: + SOLX_LIT_TARGET: ${{ matrix.target || '' }} + run: | + LIT=llvm-lit${{ runner.os == 'Windows' && '.py' || '' }} + ${LIT} -v solx-mlir/tests/lit/ actionlint: name: Lint GitHub Actions workflows @@ -297,8 +347,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: zizmor --format github .github - # Special job that allows some of the jobs to be skipped or failed - # requiring others to be successful pr-checks: name: PR Checks (Tests) runs-on: ubuntu-24.04 @@ -316,8 +364,11 @@ jobs: uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1 with: jobs: ${{ toJSON(needs) }} - # Docs-only PRs may skip the cargo-level checks, but if the change - # touches the CLI guide or its fixtures, build-and-test (which runs - # the docs_examples test) must still pass. allowed-skips: >- - ${{ needs.changes.outputs.code != 'true' && (needs.changes.outputs.docs-examples == 'true' && 'cargo-checks, cooldown-check, renovate-config-check' || 'cargo-checks, build-and-test, cooldown-check, renovate-config-check') || '' }} + ${{ + needs.changes.outputs.code != 'true' + && 'cargo-checks, build-and-test, cooldown-check, renovate-config-check' + || github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork + && 'build-and-test, cooldown-check' + || '' + }} diff --git a/.gitmodules b/.gitmodules index 6f3ec6adf..89dba3316 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "solidity"] path = solx-solidity url = https://github.com/NomicFoundation/solx-solidity - branch = 0.8.34 + branch = main [submodule "solx-llvm"] path = solx-llvm url = https://github.com/NomicFoundation/solx-llvm diff --git a/solx-dev/src/test/mod.rs b/solx-dev/src/test/mod.rs index a978de0d2..0b636f146 100644 --- a/solx-dev/src/test/mod.rs +++ b/solx-dev/src/test/mod.rs @@ -9,7 +9,7 @@ pub mod hardhat; pub mod solx_tester; /// Codegen variants every enabled compiler is tested with. -pub(crate) const CODEGENS: [&str; 2] = ["legacy", "viaIR"]; +pub(crate) const CODEGENS: [&str; 1] = ["legacy"]; /// /// The toolchain identifier that benchmark inputs, correctness tables, and diff --git a/solx-solidity b/solx-solidity index ebeac7c25..e8565c9de 160000 --- a/solx-solidity +++ b/solx-solidity @@ -1 +1 @@ -Subproject commit ebeac7c257d5fb99eab284fc57077889ef06bf5c +Subproject commit e8565c9def898a1f2253b931c35f6e35eb9626a4 From 07130ac84b83f22480b7013a4367fb802e8dd3b0 Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Thu, 30 Jul 2026 17:58:45 +0400 Subject: [PATCH 02/11] ci: fold benchmark mirror into the build-llvm composite action (#619) --- .cargo/config.toml | 10 ++-- .devcontainer/devcontainer.json | 3 - .devcontainer/post-create.sh | 2 +- .devcontainer/smoke-test.sh | 2 +- .github/actions/build-rust/action.yml | 4 +- .github/actions/build-toolchain/action.yml | 13 +++-- .github/actions/rust-unit-tests/action.yml | 2 +- .github/workflows/cache-warmup.yaml | 25 ++------ .github/workflows/compile-benchmark.yaml | 4 +- .github/workflows/coverage.yaml | 57 ++++--------------- .github/workflows/integration-tests.yaml | 52 +++-------------- .github/workflows/sanitizer.yaml | 21 ++++++- .github/workflows/test.yaml | 20 ------- CLAUDE.md | 2 +- Cargo.lock | 11 ++-- deny.toml | 1 + .../00-development-container.md | 1 - solx-mlir/Cargo.toml | 2 +- solx-mlir/build.rs | 12 ++-- solx-mlir/tests/lit/lit.cfg.py | 2 +- 20 files changed, 83 insertions(+), 163 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 111b88574..534a2ed72 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -30,12 +30,12 @@ BOOST_PREFIX = { value = "./solx-solidity/boost/lib/", relative = true, force = LLVM_SYS_211_PREFIX = { value = "./target-llvm/target-final/", relative = true, force = true } MLIR_SYS_210_PREFIX = { value = "./target-llvm/target-final/", relative = true, force = true } -TABLEGEN_210_PREFIX = { value = "./target-llvm/target-final/", relative = true, force = true } +TABLEGEN_210_PREFIX = { value = "./target-llvm/target-final/", relative = true } [alias] run-tester = "run --bin solx-tester -- --solidity-compiler ./target/release/solx" -clippy-slang = "clippy -p solx-slang -p solx-mlir -p solx --no-default-features --features slang --target-dir target-slang" -build-slang = "build -p solx-slang -p solx-mlir -p solx --no-default-features --features slang --target-dir target-slang" -test-slang = "test -p solx-slang -p solx-mlir -p solx --no-default-features --features slang --target-dir target-slang" -run-tester-slang = "run --bin solx-tester --no-default-features --features slang-ast --target-dir target-slang -- --solidity-compiler ./target-slang/release/solx -O M3B3" +clippy-slang = "clippy -p solx-slang -p solx-mlir -p solx --no-default-features --features slang" +build-slang = "build -p solx-slang -p solx-mlir -p solx --no-default-features --features slang" +test-slang = "test -p solx-slang -p solx-mlir -p solx --no-default-features --features slang" +run-tester-slang = "run --bin solx-tester --no-default-features --features slang-ast -- --solidity-compiler ./target/release/solx -O M3B3" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a435095ea..82dc37c68 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -28,7 +28,6 @@ // in the guide's Persistence section. "mounts": [ "source=solx-target-${devcontainerId},target=${containerWorkspaceFolder}/target,type=volume", - "source=solx-target-slang-${devcontainerId},target=${containerWorkspaceFolder}/target-slang,type=volume", "source=solx-target-llvm-${devcontainerId},target=${containerWorkspaceFolder}/target-llvm,type=volume", "source=solx-rustup,target=/usr/local/rustup,type=volume", "source=solx-cargo,target=/usr/local/cargo,type=volume", @@ -63,14 +62,12 @@ // lag in integrated terminals after the first bootstrap. "files.watcherExclude": { "**/target/**": true, - "**/target-slang/**": true, "**/target-llvm/**": true, "**/solx-solidity/build/**": true, "**/solx-solidity/boost/**": true }, "search.exclude": { "**/target": true, - "**/target-slang": true, "**/target-llvm": true, "**/solx-solidity/build": true, "**/solx-solidity/boost": true diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index ee50e724d..3cac7817f 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -6,7 +6,7 @@ set -euo pipefail # Named volumes are created root-owned; hand them to the dev user. sudo chown "$(id -u):$(id -g)" \ - target target-slang target-llvm \ + target target-llvm \ /var/cache/solx-ccache /usr/local/rustup /usr/local/cargo # Docker Desktop file sharing can surface the bind mount with foreign diff --git a/.devcontainer/smoke-test.sh b/.devcontainer/smoke-test.sh index 1349d856f..22cbd8faf 100755 --- a/.devcontainer/smoke-test.sh +++ b/.devcontainer/smoke-test.sh @@ -12,7 +12,7 @@ test "$(id -u)" -ne 0 sudo -n true echo "==> Workspace and named volumes are writable" -for dir in . target target-slang target-llvm "${CCACHE_DIR:?CCACHE_DIR not set}"; do +for dir in . target target-llvm "${CCACHE_DIR:?CCACHE_DIR not set}"; do touch "${dir}/.devcontainer-smoke" rm "${dir}/.devcontainer-smoke" done diff --git a/.github/actions/build-rust/action.yml b/.github/actions/build-rust/action.yml index 6e62447ce..ccae95ca0 100644 --- a/.github/actions/build-rust/action.yml +++ b/.github/actions/build-rust/action.yml @@ -81,9 +81,9 @@ runs: fi ${SFW_PREFIX:-} cargo ${BUILD_STD_LIB} build-slang --frozen ${RELEASE} ${{ steps.build-target.outputs.target }} if [ '${{ inputs.target }}' != '' ]; then - BINARY_DIR="${PWD}/target-slang/${{ inputs.target }}/${{ inputs.build-type }}" + BINARY_DIR="${PWD}/target/${{ inputs.target }}/${{ inputs.build-type }}" else - BINARY_DIR="${PWD}/target-slang/${{ inputs.build-type }}" + BINARY_DIR="${PWD}/target/${{ inputs.build-type }}" fi [ "$RUNNER_OS" = "Windows" ] && WIN_SUFFIX=".exe" echo "${BINARY_DIR}" >> "${GITHUB_PATH}" diff --git a/.github/actions/build-toolchain/action.yml b/.github/actions/build-toolchain/action.yml index e46129cea..59a4dab3e 100644 --- a/.github/actions/build-toolchain/action.yml +++ b/.github/actions/build-toolchain/action.yml @@ -5,9 +5,9 @@ description: >- on macOS), then the LLVM and solc builds. The build parameters below define the standard-config cache keys shared by test.yaml and cache-warmup.yaml — changing them here re-keys both workflows together. - Workflows needing a different LLVM - configuration (sanitizer, coverage, integration, release) call - build-llvm/build-solc directly and pair with their own cache-warmup job. + Workflows needing a different LLVM configuration (sanitizer, coverage, + integration, release) call build-llvm and build-solc directly as needed + and pair with their own cache-warmup job. inputs: llvm: @@ -73,10 +73,15 @@ runs: shell: bash run: rm -fv "$(rustc --print sysroot)/bin/libwinpthread-1.dll" + # TODO: remove this step once the lit tests stop running against solc. + # + # Release: RelWithDebInfo bakes -g3 debug info into the statically + # linked solc.exe, producing a ~2 GB PE that the Windows loader + # rejects ("Exec format error"), and the lit tests execute solc. - name: Build solc if: inputs.solc == 'true' uses: ./.github/actions/build-solc with: - cmake-build-type: RelWithDebInfo + cmake-build-type: Release working-dir: 'solx-solidity' enable-mlir: 'true' diff --git a/.github/actions/rust-unit-tests/action.yml b/.github/actions/rust-unit-tests/action.yml index 5ad39885c..978c7ede0 100644 --- a/.github/actions/rust-unit-tests/action.yml +++ b/.github/actions/rust-unit-tests/action.yml @@ -131,7 +131,7 @@ runs: export "CARGO_TARGET_${TARGET_UPPER}_LINKER=clang" fi if [ '${{ inputs.target }}' != '' ]; then - export CARGO_BIN_EXE_SOLX="./${CARGO_TARGET_DIR:-target}/${{ inputs.target }}/debug/solx" + export CARGO_BIN_EXE_SOLX="./target/${{ inputs.target }}/debug/solx" fi set -o pipefail if [ "${{ inputs.cargo-test-command }}" = "test" ]; then ${SFW_PREFIX:-} cargo build --frozen ${{ steps.build-target.outputs.target }}; fi diff --git a/.github/workflows/cache-warmup.yaml b/.github/workflows/cache-warmup.yaml index 17b5b77d7..755526075 100644 --- a/.github/workflows/cache-warmup.yaml +++ b/.github/workflows/cache-warmup.yaml @@ -7,7 +7,7 @@ name: Cache Warmup on: push: branches: - - main + - main-slang paths: - 'solx-llvm' - 'solx-solidity' @@ -140,7 +140,7 @@ jobs: with: path: ${{ runner.temp }}/ccache-solc key: ccache-touch-${{ github.job }}-${{ github.run_id }} - restore-keys: solc-${{ runner.os }}-${{ runner.arch }}-RelWithDebInfo-end + restore-keys: solc-${{ runner.os }}-${{ runner.arch }}-Release-end fail-on-cache-miss: false lookup-only: true @@ -234,13 +234,13 @@ jobs: fail-on-cache-miss: false lookup-only: true - # ── LLVM + solc: integration config (integration-tests.yaml) ──────── + # ── LLVM: integration config (integration-tests.yaml) ─────────────── warm-llvm-integration: runs-on: ubuntu-24.04 container: image: ghcr.io/nomicfoundation/solx-ci-runner@sha256:cd5a37f2630fdf1898ddb2ca11f8c2ecc4d572c60011fe923b27b1625f92ba15 volumes: *free-disk-volumes - name: "LLVM + solc · Integration" + name: "LLVM · Integration" steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -277,20 +277,3 @@ jobs: restore-keys: llvm-${{ runner.os }}-${{ runner.arch }}-mlir-no-assertions-end fail-on-cache-miss: false lookup-only: true - - - name: Build solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: Release - working-dir: 'solx-solidity' - enable-mlir: 'true' - - - name: Touch solc ccache - if: always() - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ runner.temp }}/ccache-solc - key: ccache-touch-${{ github.job }}-${{ github.run_id }} - restore-keys: solc-${{ runner.os }}-${{ runner.arch }}-Release-end - fail-on-cache-miss: false - lookup-only: true diff --git a/.github/workflows/compile-benchmark.yaml b/.github/workflows/compile-benchmark.yaml index 6634f66d2..2cd2fdc5c 100644 --- a/.github/workflows/compile-benchmark.yaml +++ b/.github/workflows/compile-benchmark.yaml @@ -164,8 +164,8 @@ jobs: - name: Run benchmark run: | ./tests/benchmark/run.sh \ - --bin pr=./target-slang/x86_64-unknown-linux-gnu/release/solx \ - --bin main=./temp-solx-main/target-slang/x86_64-unknown-linux-gnu/release/solx \ + --bin pr=./target/x86_64-unknown-linux-gnu/release/solx \ + --bin main=./temp-solx-main/target/x86_64-unknown-linux-gnu/release/solx \ --bin release=./solx-release \ --runs 3 --warmup 1 \ --out benchmark-out diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index c3a13efe8..63ba05abc 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -3,12 +3,12 @@ name: Code coverage on: pull_request: branches: - - main - # Main runs upload the Codecov base for PR deltas and save the cargo cache - # that PR runs restore (rust-cache save-if only fires on refs/heads/main). + - main-slang* + # Trunk runs upload the Codecov base for PR deltas and save the cargo cache + # that PR runs restore (rust-cache save-if only fires on refs/heads/main-slang). push: branches: - - main + - main-slang concurrency: group: ${{ github.repository_id }}-${{ github.workflow }}-${{ github.ref }} @@ -54,8 +54,6 @@ jobs: - /usr/local/.ghcup:/mnt/free-disk-space/ghcup - /opt/hostedtoolcache/CodeQL:/mnt/free-disk-space/codeql env: - BOOST_PREFIX: ${{ github.workspace }}/solx-solidity/boost/lib - SOLC_PREFIX: ${{ github.workspace }}/solx-solidity/build PROFDATA_FILE: solx.profdata LCOV_FILE: codecov.lcov OUTPUT_HTML_DIR: COVERAGE @@ -85,7 +83,7 @@ jobs: with: prefix-key: coverage-v1 cache-on-failure: true - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main-slang' }} - name: Build LLVM uses: ./.github/actions/build-llvm @@ -95,13 +93,6 @@ jobs: enable-coverage: 'true' enable-mlir: 'true' - - name: Building solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: RelWithDebInfo - working-dir: 'solx-solidity' - enable-mlir: 'true' - - name: Build solx id: build-solx uses: ./.github/actions/build-rust @@ -110,6 +101,12 @@ jobs: enable-coverage: 'true' target: 'x86_64-unknown-linux-gnu' + - name: Build solx-dev and solx-tester + env: + RUSTC_BOOTSTRAP: 1 + RUSTFLAGS: '-C target-feature=+crt-static -C instrument-coverage -C link-arg=-fuse-ld=lld' + run: ${SFW_PREFIX:-} cargo build --frozen --release -p solx-dev -p solx-tester --features solx-tester/slang-ast --target x86_64-unknown-linux-gnu + - name: Run integration tests env: SOLX_BINARY: ${{ steps.build-solx.outputs.binary-path }} @@ -122,7 +119,7 @@ jobs: "${TARGET_DIR}/solx-dev" test solx-tester \ --binary "${TARGET_DIR}/solx-tester" \ --solidity-compiler "${SOLX_BINARY}" \ - --path "${TEST_PATH}" --workflow build --optimizer M3B3 + --path "${TEST_PATH}" --workflow build --optimizer M3B3 || true mv *.profraw "${GITHUB_WORKSPACE}/${WORKDIR}/" || true find "${GITHUB_WORKSPACE}/${WORKDIR}" -type f -name '*.profraw' -print > profiles.lst if [[ -f "${PROFDATA_FILE}" ]]; then @@ -150,36 +147,8 @@ jobs: # pool of 4 files instead of leaving one profraw per process (CLI # tests spawn thousands of short-lived solx subprocesses). LLVM_PROFILE_FILE: 'unit-tests-%4m.profraw' - run: ${SFW_PREFIX:-} cargo fetch --locked && ${SFW_PREFIX:-} cargo test --frozen --release --target x86_64-unknown-linux-gnu - - # Inkwell feature poisoning: solc path builds inkwell with LLVM linking, - # slang path needs no-llvm-linking. cargo clean cannot reliably clear - # cached llvm-sys build script output. Use a separate target directory - # for complete isolation. - - name: Run slang unit tests - if: contains(github.event.pull_request.labels.*.name, 'ci:slang') - env: - RUSTC_BOOTSTRAP: 1 - RUSTFLAGS: '-C instrument-coverage -C link-arg=-fuse-ld=lld' - LLVM_PROFILE_FILE: 'slang-tests-%4m.profraw' - CARGO_TARGET_DIR: target-slang run: ${SFW_PREFIX:-} cargo test-slang --frozen --release --target x86_64-unknown-linux-gnu - - name: Preserve slang instrumented binary - if: contains(github.event.pull_request.labels.*.name, 'ci:slang') - run: cp "./target-slang/x86_64-unknown-linux-gnu/release/solx" "${GITHUB_WORKSPACE}/solx-slang-instrumented" - - - name: Merge slang test profraw - if: contains(github.event.pull_request.labels.*.name, 'ci:slang') - run: | - find . -name 'slang-tests-*.profraw' -print > slang-profiles.lst - if [ -s slang-profiles.lst ]; then - TMP="${PROFDATA_FILE}.tmp" - llvm-profdata merge -sparse -num-threads="$(nproc)" -o "${TMP}" "${PROFDATA_FILE}" @slang-profiles.lst - mv -f "${TMP}" "${PROFDATA_FILE}" - find . -name 'slang-tests-*.profraw' -delete - fi - - name: Merge unit test profraw run: | find . -name 'unit-tests-*.profraw' -print > unit-profiles.lst @@ -197,7 +166,6 @@ jobs: -object "${GITHUB_WORKSPACE}/solx-tester-instrumented" -object "${GITHUB_WORKSPACE}/solx-dev-instrumented" ) - [ -f "${GITHUB_WORKSPACE}/solx-slang-instrumented" ] && OBJECTS+=(-object "${GITHUB_WORKSPACE}/solx-slang-instrumented") llvm-cov show --show-directory-coverage \ --format=html --output-dir=${OUTPUT_HTML_DIR} \ --ignore-filename-regex="${COVERAGE_IGNORE_REGEX}" \ @@ -216,7 +184,6 @@ jobs: -object "${GITHUB_WORKSPACE}/solx-tester-instrumented" -object "${GITHUB_WORKSPACE}/solx-dev-instrumented" ) - [ -f "${GITHUB_WORKSPACE}/solx-slang-instrumented" ] && OBJECTS+=(-object "${GITHUB_WORKSPACE}/solx-slang-instrumented") llvm-cov report -instr-profile=${PROFDATA_FILE} "${OBJECTS[@]}" \ --ignore-filename-regex="${COVERAGE_IGNORE_REGEX}" \ > coverage-report.txt diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 9a2246051..006ebd1e7 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -98,52 +98,21 @@ jobs: enable-assertions: 'false' enable-mlir: 'true' - # Submodule SHA alone is not sufficient: `solx-dev llvm build` is - # driven by `solx-dev/src/llvm/*`, so a PR that edits those scripts - # without bumping the solx-llvm submodule would silently share its own - # LLVM as the main-slang baseline. - - name: Share LLVM with main-slang checkout - id: share-llvm - continue-on-error: true - run: | - # Content hash of a directory tree (order-independent). - hash_tree() { - find "$1" -type f -print0 | sort -z | xargs -0 cat | sha256sum | cut -d' ' -f1 - } - MAIN_SHA=$(git -C temp-solx-main/solx-llvm rev-parse HEAD) - PR_SHA=$(git -C solx-llvm rev-parse HEAD) - SCRIPT_PR=$(hash_tree solx-dev/src/llvm) - SCRIPT_MAIN=$(hash_tree temp-solx-main/solx-dev/src/llvm) - if [ "$MAIN_SHA" = "$PR_SHA" ] && [ "$SCRIPT_MAIN" = "$SCRIPT_PR" ]; then - echo "LLVM SHAs and build scripts match ($PR_SHA / ${SCRIPT_PR:0:8}), sharing via symlink" - mkdir -p temp-solx-main/target-llvm - rm -rf temp-solx-main/target-llvm/target-final - ln -snf "$GITHUB_WORKSPACE/target-llvm/target-final" \ - temp-solx-main/target-llvm/target-final - echo "shared=true" >> "$GITHUB_OUTPUT" - else - echo "::notice::LLVM inputs differ (main=$MAIN_SHA/${SCRIPT_MAIN:0:8} PR=$PR_SHA/${SCRIPT_PR:0:8}), building main separately" - fi - - # If share-llvm failed entirely (output empty), fall through and build. - name: Build LLVM (main-slang) - if: steps.share-llvm.outputs.shared != 'true' + uses: ./.github/actions/build-llvm continue-on-error: true - run: | - pushd temp-solx-main - ${SFW_PREFIX:-} cargo fetch --locked - ${SFW_PREFIX:-} cargo build --frozen --release --bin solx-dev - ./target/release/solx-dev llvm build --build-type Release --enable-mlir --install-distribution - popd + with: + working-dir: temp-solx-main + mirror-key: ${{ steps.build-llvm-pr.outputs.cache-key }} + build-type: Release + enable-assertions: 'false' + enable-mlir: 'true' - name: Fetch dependencies (PR) run: ${SFW_PREFIX:-} cargo fetch --locked - name: Build solx (PR) - run: | - ${SFW_PREFIX:-} cargo build-slang --frozen --release - mkdir -p target/release - cp target-slang/release/solx target/release/solx + run: ${SFW_PREFIX:-} cargo build-slang --frozen --release - name: Build solx-dev and solx-tester (PR) run: ${SFW_PREFIX:-} cargo build --frozen --release -p solx-dev -p solx-tester --features solx-tester/slang-ast @@ -156,10 +125,7 @@ jobs: - name: Build solx (main-slang) working-directory: temp-solx-main continue-on-error: true - run: | - ${SFW_PREFIX:-} cargo build-slang --frozen --release - mkdir -p target/release - cp target-slang/release/solx target/release/solx + run: ${SFW_PREFIX:-} cargo build-slang --frozen --release - name: Build solx-dev and solx-tester (main-slang) working-directory: temp-solx-main diff --git a/.github/workflows/sanitizer.yaml b/.github/workflows/sanitizer.yaml index 10f0fd3dd..49a23ae00 100644 --- a/.github/workflows/sanitizer.yaml +++ b/.github/workflows/sanitizer.yaml @@ -70,7 +70,7 @@ jobs: TARGET: x86_64-unknown-linux-gnu RUST_SANITIZER: 'address' LLVM_SANITIZER: 'Address' - CARGO_TARGET_DIR: target-slang + TABLEGEN_210_PREFIX: ${{ github.workspace }}/target-llvm-tablegen steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -86,6 +86,25 @@ jobs: with: optional: 'true' + # tblgen links libLLVMTableGen into the melior proc macro, which rustc + # dlopens without an ASan runtime: sanitized TableGen leaves __asan_* + # undefined and the load failure surfaces as E0463 at `Compiling melior`. + # Hand TABLEGEN_210_PREFIX the standard uninstrumented LLVM, built with + # build-toolchain's inputs so it restores from the warm standard cache; + # the sanitized build below covers everything the test executables link. + - name: Build LLVM for TableGen + uses: ./.github/actions/build-llvm + with: + build-type: RelWithDebInfo + enable-assertions: 'true' + enable-mlir: 'true' + enable-utils: 'true' + + - name: Move TableGen LLVM aside + run: | + mv target-llvm/target-final target-llvm-tablegen + rm -rf target-llvm/build-final + - name: Build LLVM uses: ./.github/actions/build-llvm with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f458125d7..e8cd24600 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -186,7 +186,6 @@ jobs: checks: write packages: read env: - CARGO_TARGET_DIR: target-slang CARGO_INCREMENTAL: "0" strategy: fail-fast: false @@ -230,23 +229,8 @@ jobs: if: runner.os == 'Linux' && matrix.image != '' uses: ./.github/actions/free-disk-space-linux - # solc is built separately below: the lit tests need a Release build, - # not the standard config. - name: Build toolchain uses: ./.github/actions/build-toolchain - with: - solc: 'false' - - # TODO: remove this step once the lit tests stop running against solc. - # - # Release: RelWithDebInfo bakes -g3 debug info into the statically - # linked solc.exe, producing a ~2 GB PE that the Windows loader - # rejects ("Exec format error"), and the lit tests execute solc. - - name: Build solc - uses: ./.github/actions/build-solc - with: - cmake-build-type: Release - enable-mlir: 'true' # bindgen's embedded clang needs to know it's targeting MinGW, # otherwise it can't parse MinGW-specific __attribute__ extensions @@ -282,8 +266,6 @@ jobs: uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: prefix-key: slang-tests-v2 - # `cargo test-slang` writes to target-slang, not target (.cargo/config.toml alias) - workspaces: ". -> target-slang" cache-on-failure: true save-if: ${{ github.ref == 'refs/heads/main-slang' }} @@ -292,8 +274,6 @@ jobs: with: target: ${{ matrix.target || '' }} cargo-test-command: 'test-slang' - results-xml: 'unit-tests-results.xml' - check-name: 'Unit Tests Results' - name: Verify lit tools run outside MSYS2 if: runner.os == 'Windows' diff --git a/CLAUDE.md b/CLAUDE.md index e0d3447c0..7298213b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,7 +90,7 @@ cargo test --test mod -- cli::bin::default # specific test ```bash cargo test-slang -# expands to: cargo test -p solx-slang -p solx-mlir -p solx --no-default-features --features slang --target-dir target-slang +# expands to: cargo test -p solx-slang -p solx-mlir -p solx --no-default-features --features slang ``` ### Integration tests with solx-tester diff --git a/Cargo.lock b/Cargo.lock index 9ca517399..df832d6c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -821,7 +821,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "log", "prettyplease", "proc-macro2", @@ -2776,7 +2776,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "melior" version = "0.26.9" -source = "git+https://github.com/NomicFoundation/melior?rev=52fa2611004e9553803de8dd5e6d44bb327af2da#52fa2611004e9553803de8dd5e6d44bb327af2da" +source = "git+https://github.com/NomicFoundation/melior?rev=1e6f01701b344bec2458ac938b661db669cf27a2#1e6f01701b344bec2458ac938b661db669cf27a2" dependencies = [ "melior-macro", "mlir-sys", @@ -2785,7 +2785,7 @@ dependencies = [ [[package]] name = "melior-macro" version = "0.19.5" -source = "git+https://github.com/NomicFoundation/melior?rev=52fa2611004e9553803de8dd5e6d44bb327af2da#52fa2611004e9553803de8dd5e6d44bb327af2da" +source = "git+https://github.com/NomicFoundation/melior?rev=1e6f01701b344bec2458ac938b661db669cf27a2#1e6f01701b344bec2458ac938b661db669cf27a2" dependencies = [ "comrak", "convert_case 0.11.0", @@ -5137,8 +5137,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tblgen" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4e3abe8582a0bb8708d11142c45e8c622f013bc4936d918da42c2aade60e98" +source = "git+https://github.com/hedgar2017/tblgen-rs?rev=56ad078a9bfc3ee785d56ef63bb923425ce1e088#56ad078a9bfc3ee785d56ef63bb923425ce1e088" dependencies = [ "bindgen", "cc", @@ -5153,7 +5152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.59.0", diff --git a/deny.toml b/deny.toml index faa77de70..200d19055 100644 --- a/deny.toml +++ b/deny.toml @@ -81,4 +81,5 @@ allow-git = [ "https://github.com/NomicFoundation/llvm-sys.rs", "https://github.com/NomicFoundation/melior", "https://github.com/NomicFoundation/slang", + "https://github.com/hedgar2017/tblgen-rs", ] diff --git a/docs/src/developer-guide/00-development-container.md b/docs/src/developer-guide/00-development-container.md index 7292d8e06..897017566 100644 --- a/docs/src/developer-guide/00-development-container.md +++ b/docs/src/developer-guide/00-development-container.md @@ -88,7 +88,6 @@ Build state lives in Docker named volumes so it survives **Rebuild Container** a | Volume | Mount point | Holds | |---|---|---| | `solx-target-` | `target/` | solx build artifacts, `solx-dev` | -| `solx-target-slang-` | `target-slang/` | `cargo *-slang` alias artifacts | | `solx-target-llvm-` | `target-llvm/` | LLVM build tree + installation | | `solx-rustup` | `/usr/local/rustup` | downloaded Rust toolchains | | `solx-cargo` | `/usr/local/cargo` | cargo registry/git caches | diff --git a/solx-mlir/Cargo.toml b/solx-mlir/Cargo.toml index 4a7c60dd7..f4e58fb0c 100644 --- a/solx-mlir/Cargo.toml +++ b/solx-mlir/Cargo.toml @@ -13,7 +13,7 @@ cc = "1.2.60" anyhow.workspace = true clap.workspace = true num.workspace = true -melior = { git = "https://github.com/NomicFoundation/melior", rev = "52fa2611004e9553803de8dd5e6d44bb327af2da", features = ["ods-dialects", "helpers"] } +melior = { git = "https://github.com/NomicFoundation/melior", rev = "1e6f01701b344bec2458ac938b661db669cf27a2", features = ["ods-dialects", "helpers"] } serde.workspace = true # mlir-sys 210 wraps LLVM 21.0 C API; llvm-sys (via inkwell) targets 21.1. # Both are built from the same LLVM source tree (LLVM_SYS_211_PREFIX == diff --git a/solx-mlir/build.rs b/solx-mlir/build.rs index 07a4d958c..66a384b53 100644 --- a/solx-mlir/build.rs +++ b/solx-mlir/build.rs @@ -12,10 +12,14 @@ fn main() { println!("cargo:rustc-link-search=native={}", lib_path.display()); // LLD C API — provides LLVMAssembleEVM used by inkwell's assemble_evm. - // LLVM libs are already linked by mlir-sys; only the LLD linker libs are missing. - println!("cargo:rustc-link-lib=static=lldC"); - println!("cargo:rustc-link-lib=static=lldCommon"); - println!("cargo:rustc-link-lib=static=lldELF"); + // LLVM libs are already linked by mlir-sys; only the LLD linker libs are + // missing. Not `static=`: that bundles the archives into this crate's + // rlib, which precedes libinkwell in the final link, and the sanitizer + // job's ld.bfd resolves archives in one pass, leaving inkwell's + // references undefined. Plain `-l` flags land after every rlib. + println!("cargo:rustc-link-lib=lldC"); + println!("cargo:rustc-link-lib=lldELF"); + println!("cargo:rustc-link-lib=lldCommon"); // Sol dialect — custom Solidity MLIR dialect defined in solx-llvm. println!("cargo:rustc-link-lib=static=MLIRSolDialect"); diff --git a/solx-mlir/tests/lit/lit.cfg.py b/solx-mlir/tests/lit/lit.cfg.py index 3013e0861..e0b814881 100644 --- a/solx-mlir/tests/lit/lit.cfg.py +++ b/solx-mlir/tests/lit/lit.cfg.py @@ -7,7 +7,7 @@ config_dir = os.path.dirname(os.path.abspath(__file__)) solx_root = os.path.join(config_dir, "..", "..", "..") -solx_bin_dir = os.path.join(solx_root, "target-slang", os.environ.get("SOLX_LIT_TARGET", ""), "debug") +solx_bin_dir = os.path.join(solx_root, "target", os.environ.get("SOLX_LIT_TARGET", ""), "debug") solc_bin_dir = os.path.join(solx_root, "solx-solidity", "build", "solc") config.environment["PATH"] = os.pathsep.join( From 540b2665dd1777771f046ef516e1a8dcd2cc3083 Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Thu, 30 Jul 2026 19:30:38 +0400 Subject: [PATCH 03/11] feat(slang): complete enums and user-defined value types (#614) --- Cargo.lock | 67 +++++----- Cargo.toml | 2 +- solx-mlir/sol_attr_stubs.cpp | 8 ++ solx-mlir/src/ffi.rs | 6 + solx-mlir/src/ir/mod.rs | 3 + solx-mlir/src/ir/type/mod.rs | 11 ++ solx-mlir/src/ir/value.rs | 15 ++- solx-mlir/tests/lit/comparison.sol | 44 +++++++ .../tests/lit/enum_storage_key_field.sol | 65 ++++++++++ solx-mlir/tests/lit/enum_variant.sol | 38 ++++++ solx-mlir/tests/lit/named_returns.sol | 8 ++ solx-mlir/tests/lit/type_casts.sol | 24 ++++ solx-mlir/tests/lit/user_defined_operator.sol | 115 ++++++++++++++++++ .../tests/lit/user_defined_value_type.sol | 51 ++++++-- solx-slang/Cargo.toml | 3 +- .../function/expression/arithmetic.rs | 7 ++ .../contract/function/expression/bitwise.rs | 10 ++ .../contract/function/expression/call/mod.rs | 68 +++++++---- .../function/expression/comparison.rs | 7 ++ .../contract/function/expression/member.rs | 22 +++- .../src/contract/function/expression/unary.rs | 4 + solx-slang/src/contract/mod.rs | 22 +++- solx-slang/src/source_unit.rs | 34 +++++- 23 files changed, 543 insertions(+), 91 deletions(-) create mode 100644 solx-mlir/tests/lit/enum_storage_key_field.sol create mode 100644 solx-mlir/tests/lit/enum_variant.sol create mode 100644 solx-mlir/tests/lit/user_defined_operator.sol diff --git a/Cargo.lock b/Cargo.lock index df832d6c2..4bf56811d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,7 +218,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -229,7 +229,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -286,7 +286,7 @@ dependencies = [ "fnv", "hashbrown 0.15.5", "itertools 0.13.0", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "zeroize", @@ -303,7 +303,7 @@ dependencies = [ "ark-serialize 0.3.0", "ark-std 0.3.0", "derivative", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "rustc_version 0.3.3", @@ -323,7 +323,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "rustc_version 0.4.1", @@ -344,7 +344,7 @@ dependencies = [ "digest 0.10.7", "educe", "itertools 0.13.0", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "paste", "zeroize", @@ -362,7 +362,7 @@ dependencies = [ "ark-std 0.6.0", "digest 0.10.7", "educe", - "num-bigint 0.4.6", + "num-bigint", "num-traits", "zeroize", ] @@ -413,7 +413,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "quote", "syn 1.0.109", @@ -425,7 +425,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -438,7 +438,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -451,7 +451,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-traits", "proc-macro2", "quote", @@ -484,7 +484,7 @@ dependencies = [ "ark-relations", "ark-std 0.5.0", "educe", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "tracing", @@ -520,7 +520,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-std 0.4.0", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", ] [[package]] @@ -533,7 +533,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec 0.7.6", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", ] [[package]] @@ -545,7 +545,7 @@ dependencies = [ "ark-serialize-derive 0.6.0", "ark-std 0.6.0", "digest 0.10.7", - "num-bigint 0.4.6", + "num-bigint", "serde_with", ] @@ -2911,7 +2911,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-complex", "num-integer", "num-iter", @@ -2929,16 +2929,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -2980,7 +2970,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", ] @@ -3980,7 +3970,7 @@ dependencies = [ "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-traits", "parity-scale-codec", @@ -4588,7 +4578,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slang_solidity_v2" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ "slang_solidity_v2_ast", "slang_solidity_v2_common", @@ -4601,10 +4591,10 @@ dependencies = [ [[package]] name = "slang_solidity_v2_ast" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ "itertools 0.15.0", - "num-bigint 0.4.6", + "num-bigint", "num-rational", "paste", "ruint", @@ -4618,7 +4608,7 @@ dependencies = [ [[package]] name = "slang_solidity_v2_common" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ "fxhash", "indexmap 2.14.0", @@ -4632,12 +4622,12 @@ dependencies = [ [[package]] name = "slang_solidity_v2_cst" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" [[package]] name = "slang_solidity_v2_ir" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ "slang_solidity_v2_common", "slang_solidity_v2_cst", @@ -4646,7 +4636,7 @@ dependencies = [ [[package]] name = "slang_solidity_v2_parser" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ "lalrpop", "lalrpop-util", @@ -4660,9 +4650,9 @@ dependencies = [ [[package]] name = "slang_solidity_v2_semantic" version = "1.3.7" -source = "git+https://github.com/NomicFoundation/slang.git?rev=3b82850f6d525d42456135fea742ee556f56c298#3b82850f6d525d42456135fea742ee556f56c298" +source = "git+https://github.com/NomicFoundation/slang.git?rev=c32b87a8808d7659b8cc4380b9e36ec8e31d92db#c32b87a8808d7659b8cc4380b9e36ec8e31d92db" dependencies = [ - "num-bigint 0.4.6", + "num-bigint", "num-integer", "num-rational", "num-traits", @@ -4877,7 +4867,8 @@ name = "solx-slang" version = "0.1.7" dependencies = [ "anyhow", - "num-bigint 0.5.1", + "itertools 0.15.0", + "num", "num-traits", "ruint", "semver 1.0.28", diff --git a/Cargo.toml b/Cargo.toml index c175300cd..3c4e5db20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,4 +94,4 @@ features = [ [workspace.dependencies.slang_solidity_v2] git = "https://github.com/NomicFoundation/slang.git" # TODO: pin to a release tag instead of a revision. -rev = "3b82850f6d525d42456135fea742ee556f56c298" +rev = "c32b87a8808d7659b8cc4380b9e36ec8e31d92db" diff --git a/solx-mlir/sol_attr_stubs.cpp b/solx-mlir/sol_attr_stubs.cpp index 3c49d1988..049477122 100644 --- a/solx-mlir/sol_attr_stubs.cpp +++ b/solx-mlir/sol_attr_stubs.cpp @@ -137,6 +137,14 @@ bool solxIsAddressType(MlirType ty) { return mlir::isa(unwrap(ty)); } +bool solxIsEnumType(MlirType ty) { + return mlir::isa(unwrap(ty)); +} + +uint32_t solxEnumTypeMax(MlirType ty) { + return mlir::cast(unwrap(ty)).getMax(); +} + bool solxIsStringType(MlirType ty) { return mlir::isa(unwrap(ty)); } diff --git a/solx-mlir/src/ffi.rs b/solx-mlir/src/ffi.rs index ef9d3725c..f08905879 100644 --- a/solx-mlir/src/ffi.rs +++ b/solx-mlir/src/ffi.rs @@ -162,6 +162,12 @@ unsafe extern "C" { /// Whether the type is a `sol::AddressType`, regardless of payability. pub fn solxIsAddressType(ty: mlir_sys::MlirType) -> bool; + /// Whether the type is a `sol::EnumType`. + pub fn solxIsEnumType(ty: mlir_sys::MlirType) -> bool; + + /// Returns the largest ordinal a `sol::EnumType` admits. + pub fn solxEnumTypeMax(ty: mlir_sys::MlirType) -> u32; + /// Whether the type is a `sol::StringType`, the shared representation of dynamic /// `bytes` and `string`. pub fn solxIsStringType(ty: mlir_sys::MlirType) -> bool; diff --git a/solx-mlir/src/ir/mod.rs b/solx-mlir/src/ir/mod.rs index 300c87a5b..bb675838a 100644 --- a/solx-mlir/src/ir/mod.rs +++ b/solx-mlir/src/ir/mod.rs @@ -36,6 +36,9 @@ sol_ops! { Value::address_cast(self, target_type: ty) -> value { AddressCastOperation.inp(self).out(target_type) } + Value::enum_cast(self, target_type: ty) -> value { + EnumCastOperation.inp(self).out(target_type) + } Value::dyn_bytes_to_fixedbytes(self, target_type: ty) -> value { DynBytesToFixedBytesOperation.inp(self).out(target_type) } diff --git a/solx-mlir/src/ir/type/mod.rs b/solx-mlir/src/ir/type/mod.rs index 5c8440554..efa5ea53e 100644 --- a/solx-mlir/src/ir/type/mod.rs +++ b/solx-mlir/src/ir/type/mod.rs @@ -198,6 +198,17 @@ impl<'context> Type<'context> { unsafe { ffi::solxIsAddressType(self.inner.to_raw()) } } + /// Whether this is a `sol::EnumType`. + pub fn is_enum(self) -> bool { + unsafe { ffi::solxIsEnumType(self.inner.to_raw()) } + } + + /// The largest ordinal this enum type admits; the classification is the caller's, via + /// `is_enum`. + pub fn enum_max(self) -> u32 { + unsafe { ffi::solxEnumTypeMax(self.inner.to_raw()) } + } + /// Whether this is a `sol::StringType`, the shared representation of dynamic `bytes` and /// `string`. pub fn is_string(self) -> bool { diff --git a/solx-mlir/src/ir/value.rs b/solx-mlir/src/ir/value.rs index 05e9aa614..7c08bc82c 100644 --- a/solx-mlir/src/ir/value.rs +++ b/solx-mlir/src/ir/value.rs @@ -27,8 +27,9 @@ pub struct Value<'context> { impl<'context> Value<'context> { /// Materialises a `sol.constant` from an arbitrary-width [`BigInt`] at the type a constant can be - /// emitted at, then converts it to the target: `ui160` for an address, the width-matched unsigned - /// integer for a bytes-like target, the target type itself otherwise. + /// emitted at, then converts it to the target: `ui160` for an address, the EVM word for an enum + /// ordinal, the width-matched unsigned integer for a bytes-like target, the target type itself + /// otherwise. pub fn constant_from_bigint( value: &BigInt, result_type: Type<'context>, @@ -36,6 +37,8 @@ impl<'context> Value<'context> { ) -> Self { let r#type = if result_type.is_address() { Type::unsigned(context.melior, solx_utils::BIT_LENGTH_ETH_ADDRESS) + } else if result_type.is_enum() { + Type::field(context.melior) } else if result_type.is_bytes_like() { let bits = result_type.bytes_like_width() as usize * solx_utils::BIT_LENGTH_BYTE; Type::unsigned(context.melior, bits) @@ -105,12 +108,16 @@ impl<'context> Value<'context> { } /// Emits the cast reconciling `self` to `target_type`, be it a computed common type or an - /// explicit `T(x)`. The address and string-to-`bytesN` arms, reachable only under an explicit - /// cast, precede the bytes and scalar arms an address or string would otherwise fall into. + /// explicit `T(x)`. The enum, address, and string-to-`bytesN` arms, reachable only under an + /// explicit cast, precede the bytes and scalar arms an enum, address, or string would otherwise + /// fall into. pub fn convert(mut self, target_type: Type<'context>, context: &Context<'context>) -> Self { if self.r#type() == target_type { return self; } + if self.r#type().is_enum() || target_type.is_enum() { + return self.enum_cast(target_type, context); + } if self.r#type().is_address() { return self.address_cast(target_type, context); } diff --git a/solx-mlir/tests/lit/comparison.sol b/solx-mlir/tests/lit/comparison.sol index 5c376f0c6..5f09064bc 100644 --- a/solx-mlir/tests/lit/comparison.sol +++ b/solx-mlir/tests/lit/comparison.sol @@ -72,7 +72,27 @@ // CHECK: sol.func @{{.*ge_fixed_bytes.*}} // CHECK: sol.cmp ge, %{{.*}}, %{{.*}} : !sol.fixedbytes<8> +// CHECK: sol.func @{{.*eq_enum.*}} +// CHECK: sol.cmp eq, %{{.*}}, %{{.*}} : !sol.enum<2> + +// CHECK: sol.func @{{.*ne_enum.*}} +// CHECK: sol.cmp ne, %{{.*}}, %{{.*}} : !sol.enum<2> + +// CHECK: sol.func @{{.*lt_enum.*}} +// CHECK: sol.cmp lt, %{{.*}}, %{{.*}} : !sol.enum<2> + +// CHECK: sol.func @{{.*le_enum.*}} +// CHECK: sol.cmp le, %{{.*}}, %{{.*}} : !sol.enum<2> + +// CHECK: sol.func @{{.*gt_enum.*}} +// CHECK: sol.cmp gt, %{{.*}}, %{{.*}} : !sol.enum<2> + +// CHECK: sol.func @{{.*ge_enum.*}} +// CHECK: sol.cmp ge, %{{.*}}, %{{.*}} : !sol.enum<2> + contract C { + enum E { First, Second, Third } + function eq(uint256 a, uint256 b) public pure returns (bool) { return a == b; } @@ -164,4 +184,28 @@ contract C { function ge_fixed_bytes(bytes4 a, bytes8 b) public pure returns (bool) { return a >= b; } + + function eq_enum(E a, E b) public pure returns (bool) { + return a == b; + } + + function ne_enum(E a, E b) public pure returns (bool) { + return a != b; + } + + function lt_enum(E a, E b) public pure returns (bool) { + return a < b; + } + + function le_enum(E a, E b) public pure returns (bool) { + return a <= b; + } + + function gt_enum(E a, E b) public pure returns (bool) { + return a > b; + } + + function ge_enum(E a, E b) public pure returns (bool) { + return a >= b; + } } diff --git a/solx-mlir/tests/lit/enum_storage_key_field.sol b/solx-mlir/tests/lit/enum_storage_key_field.sol new file mode 100644 index 000000000..7718c7eba --- /dev/null +++ b/solx-mlir/tests/lit/enum_storage_key_field.sol @@ -0,0 +1,65 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s + +// CHECK: sol.state_var @{{.*}} slot 0 offset 0 : !sol.enum<2> +// CHECK: sol.state_var @{{.*}} slot 1 offset 0 : !sol.mapping, ui256> +// CHECK: sol.state_var @{{.*}} slot 2 offset 0 : !sol.struct<(!sol.enum<2>, ui256), Storage> + +// CHECK: sol.func @{{.*write.*}} +// CHECK: sol.addr_of @{{.*}} : !sol.ptr, Storage> +// CHECK: sol.store %{{.*}}, %{{.*}} : !sol.enum<2>, !sol.ptr, Storage> + +// CHECK: sol.func @{{.*read.*}}-> !sol.enum<2> +// CHECK: sol.addr_of @{{.*}} : !sol.ptr, Storage> +// CHECK: sol.load %{{.*}} : !sol.ptr, Storage>, !sol.enum<2> + +// CHECK: sol.func @{{.*entry.*}}-> ui256 +// CHECK: sol.map %{{.*}}, %{{.*}} : !sol.mapping, ui256>, !sol.enum<2>, !sol.ptr +// CHECK: sol.load %{{.*}} : !sol.ptr, ui256 + +// CHECK: sol.func @{{.*write_entry.*}} +// CHECK: sol.map %{{.*}}, %{{.*}} : !sol.mapping, ui256>, !sol.enum<2>, !sol.ptr +// CHECK: sol.store %{{.*}}, %{{.*}} : ui256, !sol.ptr + +// CHECK: sol.func @{{.*field.*}}-> !sol.enum<2> +// CHECK: sol.gep %{{.*}}, %{{.*}} : !sol.struct<(!sol.enum<2>, ui256), Storage>, ui64, !sol.ptr, Storage> +// CHECK: sol.load %{{.*}} : !sol.ptr, Storage>, !sol.enum<2> + +// CHECK: sol.func @{{.*clear.*}} +// CHECK: sol.constant 0 : ui256 +// CHECK: sol.enum_cast %{{.*}} : ui256 to !sol.enum<2> +// CHECK: sol.store %{{.*}}, %{{.*}} : !sol.enum<2>, !sol.ptr, Storage> + +contract C { + enum E { First, Second, Third } + + struct S { E tag; uint256 amount; } + + E state; + mapping(E => uint256) counts; + S item; + + function write(E value) public { + state = value; + } + + function read() public view returns (E) { + return state; + } + + function entry(E key) public view returns (uint256) { + return counts[key]; + } + + function write_entry(E key, uint256 value) public { + counts[key] = value; + } + + function field() public view returns (E) { + return item.tag; + } + + function clear() public { + delete state; + } +} diff --git a/solx-mlir/tests/lit/enum_variant.sol b/solx-mlir/tests/lit/enum_variant.sol new file mode 100644 index 000000000..b6df4c504 --- /dev/null +++ b/solx-mlir/tests/lit/enum_variant.sol @@ -0,0 +1,38 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s + +// CHECK: sol.func @{{.*variant.*}}-> !sol.enum<2> +// CHECK: sol.constant 1 : ui256 +// CHECK: sol.enum_cast %{{.*}} : ui256 to !sol.enum<2> + +// CHECK: sol.func @{{.*qualified_variant.*}}-> !sol.enum<2> +// CHECK: sol.constant 2 : ui256 +// CHECK: sol.enum_cast %{{.*}} : ui256 to !sol.enum<2> + +// CHECK: sol.func @{{.*type_min.*}}-> !sol.enum<2> +// CHECK: sol.constant 0 : ui256 +// CHECK: sol.enum_cast %{{.*}} : ui256 to !sol.enum<2> + +// CHECK: sol.func @{{.*type_max.*}}-> !sol.enum<2> +// CHECK: sol.constant 2 : ui256 +// CHECK: sol.enum_cast %{{.*}} : ui256 to !sol.enum<2> + +contract C { + enum E { First, Second, Third } + + function variant() public pure returns (E) { + return E.Second; + } + + function qualified_variant() public pure returns (E) { + return C.E.Third; + } + + function type_min() public pure returns (E) { + return type(E).min; + } + + function type_max() public pure returns (E) { + return type(E).max; + } +} diff --git a/solx-mlir/tests/lit/named_returns.sol b/solx-mlir/tests/lit/named_returns.sol index 330a2d78d..b278a62e9 100644 --- a/solx-mlir/tests/lit/named_returns.sol +++ b/solx-mlir/tests/lit/named_returns.sol @@ -16,7 +16,13 @@ // CHECK: %[[ZERO:.*]] = sol.constant 0 : ui32 // CHECK: sol.bytes_cast %[[ZERO]] : ui32 to !sol.fixedbytes<4> +// CHECK: sol.func @{{.*named_enum.*}} +// CHECK: %[[ORDINAL:.*]] = sol.constant 0 : ui256 +// CHECK: sol.enum_cast %[[ORDINAL]] : ui256 to !sol.enum<2> + contract C { + enum E { First, Second, Third } + function identity(bool _in) public pure returns (bool _out) { _out = _in; } @@ -26,4 +32,6 @@ contract C { } function named_bytes() public pure returns (bytes4 result) {} + + function named_enum() public pure returns (E result) {} } diff --git a/solx-mlir/tests/lit/type_casts.sol b/solx-mlir/tests/lit/type_casts.sol index 72d59c466..5777ecf0f 100644 --- a/solx-mlir/tests/lit/type_casts.sol +++ b/solx-mlir/tests/lit/type_casts.sol @@ -37,7 +37,19 @@ // CHECK: sol.func @{{.*array_to_memory.*}} // CHECK: sol.data_loc_cast %{{.*}} : !sol.array, !sol.array +// CHECK: sol.func @{{.*uint8_to_enum.*}} +// CHECK: sol.enum_cast %{{.*}} : ui8 to !sol.enum<2> + +// CHECK: sol.func @{{.*enum_to_uint8.*}} +// CHECK: sol.enum_cast %{{.*}} : !sol.enum<2> to ui8 + +// CHECK: sol.func @{{.*enum_to_uint256.*}} +// CHECK: sol.enum_cast %{{.*}} : !sol.enum<2> to ui8 +// CHECK: sol.cast %{{.*}} : ui8 to ui256 + contract C { + enum E { First, Second, Third } + function uint8_to_uint256(uint8 x) public pure returns (uint256) { return uint256(x); } @@ -86,4 +98,16 @@ contract C { function array_to_memory(uint256[] calldata source) external pure returns (uint256[] memory) { return uint256[](source); } + + function uint8_to_enum(uint8 x) public pure returns (E) { + return E(x); + } + + function enum_to_uint8(E x) public pure returns (uint8) { + return uint8(x); + } + + function enum_to_uint256(E x) public pure returns (uint256) { + return uint256(uint8(x)); + } } diff --git a/solx-mlir/tests/lit/user_defined_operator.sol b/solx-mlir/tests/lit/user_defined_operator.sol new file mode 100644 index 000000000..190917f77 --- /dev/null +++ b/solx-mlir/tests/lit/user_defined_operator.sol @@ -0,0 +1,115 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s +// solc's print-init emits the contract alone, so the CHECKs stop at the call sites. + +// CHECK: sol.func @{{.*plus.*}}-> ui256 +// CHECK: sol.call @{{.*add.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*minus.*}}-> ui256 +// CHECK: sol.call @{{.*sub.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*times.*}}-> ui256 +// CHECK: sol.call @{{.*mul.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*divided.*}}-> ui256 +// CHECK: sol.call @{{.*div.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*modulo.*}}-> ui256 +// CHECK: sol.call @{{.*rem.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*conjunction.*}}-> ui256 +// CHECK: sol.call @{{.*band.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*disjunction.*}}-> ui256 +// CHECK: sol.call @{{.*bor.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*exclusive.*}}-> ui256 +// CHECK: sol.call @{{.*bxor.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*negated.*}}-> ui256 +// CHECK: sol.call @{{.*neg.*}}(%{{.*}}) : (ui256) -> ui256 + +// CHECK: sol.func @{{.*inverted.*}}-> ui256 +// CHECK: sol.call @{{.*bnot.*}}(%{{.*}}) : (ui256) -> ui256 + +// CHECK: sol.func @{{.*equal.*}}-> i1 +// CHECK: sol.call @{{.*eq.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*unequal.*}}-> i1 +// CHECK: sol.call @{{.*ne.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*below.*}}-> i1 +// CHECK: sol.call @{{.*lt.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*at_most.*}}-> i1 +// CHECK: sol.call @{{.*le.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*above.*}}-> i1 +// CHECK: sol.call @{{.*gt.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*at_least.*}}-> i1 +// CHECK: sol.call @{{.*ge.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> i1 + +// CHECK: sol.func @{{.*precedence.*}}-> ui256 +// CHECK: sol.call @{{.*mul.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 +// CHECK: sol.call @{{.*add.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*signed_equal.*}}-> i1 +// CHECK: sol.call @{{.*signed_eq.*}}(%{{.*}}, %{{.*}}) : (si256, si256) -> i1 + +// CHECK: sol.func @{{.*signed_below.*}}-> i1 +// CHECK: sol.call @{{.*signed_lt.*}}(%{{.*}}, %{{.*}}) : (si256, si256) -> i1 + +type T is uint256; +type Signed is int256; + +using { + add as +, sub as -, mul as *, div as /, rem as %, + band as &, bor as |, bxor as ^, + neg as -, bnot as ~, + eq as ==, ne as !=, lt as <, le as <=, gt as >, ge as >= +} for T global; + +using {signed_eq as ==, signed_lt as <} for Signed global; + +function add(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) + T.unwrap(b)); } +function sub(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) - T.unwrap(b)); } +function mul(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) * T.unwrap(b)); } +function div(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) / T.unwrap(b)); } +function rem(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) % T.unwrap(b)); } +function band(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) & T.unwrap(b)); } +function bor(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) | T.unwrap(b)); } +function bxor(T a, T b) pure returns (T) { return T.wrap(T.unwrap(a) ^ T.unwrap(b)); } +function neg(T a) pure returns (T) { return T.wrap(0 - T.unwrap(a)); } +function bnot(T a) pure returns (T) { return T.wrap(~T.unwrap(a)); } +function eq(T a, T b) pure returns (bool) { return T.unwrap(a) == T.unwrap(b); } +function ne(T a, T b) pure returns (bool) { return T.unwrap(a) != T.unwrap(b); } +function lt(T a, T b) pure returns (bool) { return T.unwrap(a) < T.unwrap(b); } +function le(T a, T b) pure returns (bool) { return T.unwrap(a) <= T.unwrap(b); } +function gt(T a, T b) pure returns (bool) { return T.unwrap(a) > T.unwrap(b); } +function ge(T a, T b) pure returns (bool) { return T.unwrap(a) >= T.unwrap(b); } + +function signed_eq(Signed a, Signed b) pure returns (bool) { return Signed.unwrap(a) == Signed.unwrap(b); } +function signed_lt(Signed a, Signed b) pure returns (bool) { return Signed.unwrap(a) < Signed.unwrap(b); } + +contract C { + function plus(T a, T b) public pure returns (T) { return a + b; } + function minus(T a, T b) public pure returns (T) { return a - b; } + function times(T a, T b) public pure returns (T) { return a * b; } + function divided(T a, T b) public pure returns (T) { return a / b; } + function modulo(T a, T b) public pure returns (T) { return a % b; } + function conjunction(T a, T b) public pure returns (T) { return a & b; } + function disjunction(T a, T b) public pure returns (T) { return a | b; } + function exclusive(T a, T b) public pure returns (T) { return a ^ b; } + function negated(T a) public pure returns (T) { return -a; } + function inverted(T a) public pure returns (T) { return ~a; } + function equal(T a, T b) public pure returns (bool) { return a == b; } + function unequal(T a, T b) public pure returns (bool) { return a != b; } + function below(T a, T b) public pure returns (bool) { return a < b; } + function at_most(T a, T b) public pure returns (bool) { return a <= b; } + function above(T a, T b) public pure returns (bool) { return a > b; } + function at_least(T a, T b) public pure returns (bool) { return a >= b; } + function precedence(T a, T b, T c) public pure returns (T) { return a + b * c; } + function signed_equal(Signed a, Signed b) public pure returns (bool) { return a == b; } + function signed_below(Signed a, Signed b) public pure returns (bool) { return a < b; } +} diff --git a/solx-mlir/tests/lit/user_defined_value_type.sol b/solx-mlir/tests/lit/user_defined_value_type.sol index c3838de3b..c282977ce 100644 --- a/solx-mlir/tests/lit/user_defined_value_type.sol +++ b/solx-mlir/tests/lit/user_defined_value_type.sol @@ -1,19 +1,46 @@ // RUN: solx --emit-mlir=sol %s | FileCheck %s // RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s -// CHECK: sol.func @{{.*id_u.*}}: ui256) -> ui256 -// CHECK: sol.func @{{.*id_s.*}}: si8) -> si8 -// CHECK: sol.func @{{.*id_a.*}}: !sol.address) -> !sol.address -// CHECK: sol.func @{{.*id_b.*}}: i1) -> i1 +// CHECK: sol.func @{{.*identity_unsigned.*}}: ui256) -> ui256 +// CHECK: sol.func @{{.*identity_signed.*}}: si8) -> si8 +// CHECK: sol.func @{{.*identity_address.*}}: !sol.address) -> !sol.address +// CHECK: sol.func @{{.*identity_boolean.*}}: i1) -> i1 + +// CHECK: sol.func @{{.*wrap_unsigned.*}}: ui256) -> ui256 +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*unwrap_unsigned.*}}: ui256) -> ui256 +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*wrap_signed.*}}: si8) -> si8 +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*unwrap_signed.*}}: si8) -> si8 +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*wrap_address.*}}: !sol.address) -> !sol.address +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*unwrap_address.*}}: !sol.address) -> !sol.address +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*wrap_boolean.*}}: i1) -> i1 +// CHECK-NOT: _cast +// CHECK: sol.func @{{.*unwrap_boolean.*}}: i1) -> i1 +// CHECK-NOT: _cast +// CHECK: sol.return contract C { - type U is uint256; - type S is int8; - type A is address; - type B is bool; + type Unsigned is uint256; + type Signed is int8; + type Address is address; + type Boolean is bool; + + function identity_unsigned(Unsigned x) public pure returns (Unsigned) { return x; } + function identity_signed(Signed x) public pure returns (Signed) { return x; } + function identity_address(Address x) public pure returns (Address) { return x; } + function identity_boolean(Boolean x) public pure returns (Boolean) { return x; } - function id_u(U x) public pure returns (U) { return x; } - function id_s(S x) public pure returns (S) { return x; } - function id_a(A x) public pure returns (A) { return x; } - function id_b(B x) public pure returns (B) { return x; } + function wrap_unsigned(uint256 x) public pure returns (Unsigned) { return Unsigned.wrap(x); } + function unwrap_unsigned(Unsigned x) public pure returns (uint256) { return Unsigned.unwrap(x); } + function wrap_signed(int8 x) public pure returns (Signed) { return Signed.wrap(x); } + function unwrap_signed(Signed x) public pure returns (int8) { return Signed.unwrap(x); } + function wrap_address(address x) public pure returns (Address) { return Address.wrap(x); } + function unwrap_address(Address x) public pure returns (address) { return Address.unwrap(x); } + function wrap_boolean(bool x) public pure returns (Boolean) { return Boolean.wrap(x); } + function unwrap_boolean(Boolean x) public pure returns (bool) { return Boolean.unwrap(x); } } diff --git a/solx-slang/Cargo.toml b/solx-slang/Cargo.toml index 4d43f56b3..3c996ab87 100644 --- a/solx-slang/Cargo.toml +++ b/solx-slang/Cargo.toml @@ -11,7 +11,8 @@ doctest = false [dependencies] anyhow.workspace = true -num-bigint = "0.5" +itertools.workspace = true +num.workspace = true num-traits = "0.2" ruint.workspace = true serde_json.workspace = true diff --git a/solx-slang/src/contract/function/expression/arithmetic.rs b/solx-slang/src/contract/function/expression/arithmetic.rs index d5465cafa..cc3a741f4 100644 --- a/solx-slang/src/contract/function/expression/arithmetic.rs +++ b/solx-slang/src/contract/function/expression/arithmetic.rs @@ -9,6 +9,7 @@ use slang_solidity_v2::ast::ExponentiationExpression; use slang_solidity_v2::ast::MultiplicativeExpression; use slang_solidity_v2::ast::MultiplicativeExpressionOperator; use slang_solidity_v2::ast::PositionalArguments; +use slang_solidity_v2::ast::UserDefinedOperatorExpression; use solx_mlir::Context; use solx_mlir::Type as MlirType; @@ -19,6 +20,9 @@ use crate::scope::function::FunctionScope; impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { /// `a + b` and `a - b`, both operands converted to the binder's result type. pub fn additive(&mut self, node: &AdditiveExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let (lhs, rhs) = self.converted_operands(node.get_type(), &node.left_operand(), &node.right_operand()); match node.operator() { @@ -29,6 +33,9 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// `a * b`, `a / b`, and `a % b`, both operands converted to the binder's result type. pub fn multiplicative(&mut self, node: &MultiplicativeExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let (lhs, rhs) = self.converted_operands(node.get_type(), &node.left_operand(), &node.right_operand()); match node.operator() { diff --git a/solx-slang/src/contract/function/expression/bitwise.rs b/solx-slang/src/contract/function/expression/bitwise.rs index f2a51f0c0..761c084d4 100644 --- a/solx-slang/src/contract/function/expression/bitwise.rs +++ b/solx-slang/src/contract/function/expression/bitwise.rs @@ -7,6 +7,7 @@ use slang_solidity_v2::ast::BitwiseOrExpression; use slang_solidity_v2::ast::BitwiseXorExpression; use slang_solidity_v2::ast::ShiftExpression; use slang_solidity_v2::ast::ShiftExpressionOperator; +use slang_solidity_v2::ast::UserDefinedOperatorExpression; use solx_mlir::Value; @@ -15,6 +16,9 @@ use crate::scope::function::FunctionScope; impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { /// `a & b`, both operands converted to the binder's result type. pub fn bitwise_and(&mut self, node: &BitwiseAndExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let (lhs, rhs) = self.converted_operands(node.get_type(), &node.left_operand(), &node.right_operand()); lhs.bitand(rhs, self) @@ -22,6 +26,9 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// `a | b`, both operands converted to the binder's result type. pub fn bitwise_or(&mut self, node: &BitwiseOrExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let (lhs, rhs) = self.converted_operands(node.get_type(), &node.left_operand(), &node.right_operand()); lhs.bitor(rhs, self) @@ -29,6 +36,9 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// `a ^ b`, both operands converted to the binder's result type. pub fn bitwise_xor(&mut self, node: &BitwiseXorExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let (lhs, rhs) = self.converted_operands(node.get_type(), &node.left_operand(), &node.right_operand()); lhs.bitxor(rhs, self) diff --git a/solx-slang/src/contract/function/expression/call/mod.rs b/solx-slang/src/contract/function/expression/call/mod.rs index ffb33826c..fbc14a7ff 100644 --- a/solx-slang/src/contract/function/expression/call/mod.rs +++ b/solx-slang/src/contract/function/expression/call/mod.rs @@ -58,7 +58,7 @@ impl Call { Self::StructConstruction(struct_definition) => { Self::struct_construction(&struct_definition, node, arguments, scope) } - Self::TypeConversion => Self::type_conversion(node, arguments, scope), + Self::TypeConversion => vec![Self::type_conversion(node, arguments, scope)], Self::Builtin(built_in) => Self::builtin(built_in, arguments, scope) .into_iter() .collect(), @@ -66,7 +66,7 @@ impl Call { .into_iter() .collect(), Self::Function(function_definition) => { - Self::function(&function_definition, arguments, scope) + scope.call(&function_definition, arguments.iter()) } } } @@ -142,13 +142,13 @@ impl Call { call: &FunctionCallExpression, arguments: &PositionalArguments, scope: &mut FunctionScope<'_, '_, 'context>, - ) -> Vec> { + ) -> Value<'context> { let operand = arguments .iter() .next() .expect("classification admits exactly one argument"); let target_type = scope.typing(call.get_type()); - vec![scope.converted(&operand, target_type)] + scope.converted(&operand, target_type) } /// Statement-style built-ins (`assert`, `require`, `revert`) produce no value. @@ -402,47 +402,63 @@ impl Call { Some(BuiltIn::BytesConcat | BuiltIn::StringConcat) => { Some(Value::concat(&scope.positional_arguments(arguments), scope)) } + Some(BuiltIn::Wrap | BuiltIn::Unwrap) => { + Some(Self::type_conversion(call, arguments, scope)) + } _ => unimplemented!("unsupported member call: {}", access.member().name()), } } +} + +impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { + /// `arr.push()` is the sole call assignable in place position; the slot it grows is the value the + /// push emits, and its element type is that slot's pointee. + pub fn function_call_place( + &mut self, + node: &FunctionCallExpression, + ) -> (Place<'context>, MlirType<'context>) { + let slot = Call::emit(node, self) + .into_iter() + .next() + .expect("an array push in place position yields the new element's slot"); + (Place::from(slot), slot.r#type().element_type(0)) + } + + /// Calls the function a `using {f as op} for T global;` directive binds an operator to. Being a + /// call, its operands evaluate left-first, not in a built-in operator's right-first order. + pub fn bound_operator( + &mut self, + function_definition: &FunctionDefinition, + operands: impl IntoIterator, + ) -> Value<'context> { + self.call(function_definition, operands) + .into_iter() + .next() + .expect("a user-defined operator's function returns one value") + } /// Resolves the callee's pre-registered MLIR signature by node id and converts each argument to /// its declared parameter type before `sol.call`. - fn function<'context>( + fn call( + &mut self, function_definition: &FunctionDefinition, - arguments: &PositionalArguments, - scope: &mut FunctionScope<'_, '_, 'context>, + arguments: impl IntoIterator, ) -> Vec> { - let signature = scope + let signature = self .contract .source_unit .function_signature(function_definition.node_id()); let converted: Vec> = arguments - .iter() + .into_iter() .zip(&signature.parameter_types) - .map(|(argument, ¶meter_type)| scope.converted(&argument, parameter_type)) + .map(|(argument, ¶meter_type)| self.converted(&argument, parameter_type)) .collect(); Function::call( &signature.mlir_name, &converted, &signature.return_types, - scope, + self, ) .expect("sol.call yields its declared results") } } - -impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { - /// `arr.push()` is the sole call assignable in place position; the slot it grows is the value the - /// push emits, and its element type is that slot's pointee. - pub fn function_call_place( - &mut self, - node: &FunctionCallExpression, - ) -> (Place<'context>, MlirType<'context>) { - let slot = Call::emit(node, self) - .into_iter() - .next() - .expect("an array push in place position yields the new element's slot"); - (Place::from(slot), slot.r#type().element_type(0)) - } -} diff --git a/solx-slang/src/contract/function/expression/comparison.rs b/solx-slang/src/contract/function/expression/comparison.rs index 8930193ba..3af39be57 100644 --- a/solx-slang/src/contract/function/expression/comparison.rs +++ b/solx-slang/src/contract/function/expression/comparison.rs @@ -7,6 +7,7 @@ use slang_solidity_v2::ast::EqualityExpression; use slang_solidity_v2::ast::EqualityExpressionOperator; use slang_solidity_v2::ast::InequalityExpression; use slang_solidity_v2::ast::InequalityExpressionOperator; +use slang_solidity_v2::ast::UserDefinedOperatorExpression; use solx_mlir::CmpPredicate; use solx_mlir::Value; @@ -16,6 +17,9 @@ use crate::scope::function::FunctionScope; impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { /// `a == b` and `a != b`, the operands converted to the type the binder reconciles them to. pub fn equality(&mut self, node: &EqualityExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let predicate = match node.operator() { EqualityExpressionOperator::EqualEqual(_) => CmpPredicate::Eq, EqualityExpressionOperator::BangEqual(_) => CmpPredicate::Ne, @@ -31,6 +35,9 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// `a < b`, `a <= b`, `a > b`, and `a >= b`, the operands converted to the type the binder /// reconciles them to. pub fn inequality(&mut self, node: &InequalityExpression) -> Value<'context> { + if let Some(function) = node.resolve_operator_to_function() { + return self.bound_operator(&function, [node.left_operand(), node.right_operand()]); + } let predicate = match node.operator() { InequalityExpressionOperator::LessThan(_) => CmpPredicate::Lt, InequalityExpressionOperator::LessThanEqual(_) => CmpPredicate::Le, diff --git a/solx-slang/src/contract/function/expression/member.rs b/solx-slang/src/contract/function/expression/member.rs index d7bccc1a0..1f1b7f9b3 100644 --- a/solx-slang/src/contract/function/expression/member.rs +++ b/solx-slang/src/contract/function/expression/member.rs @@ -2,6 +2,7 @@ //! Member access expressions: struct fields and the environment intrinsics. //! +use num::BigInt; use slang_solidity_v2::ast::BuiltIn; use slang_solidity_v2::ast::Definition; use slang_solidity_v2::ast::MemberAccessExpression; @@ -14,13 +15,25 @@ use solx_mlir::Value; use crate::scope::function::FunctionScope; impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { - /// A struct field loads from its place; every other member access is an environment or EVM - /// intrinsic. + /// A struct field loads from its place; an enum member is its ordinal; every other member access + /// is an environment or EVM intrinsic. pub fn member_access(&mut self, node: &MemberAccessExpression) -> Value<'context> { if matches!(node.operand().get_type(), Some(Type::Struct(_))) { let (place, element_type) = self.member_access_place(node); return place.load(element_type, self); } + if let Some(Definition::EnumMember(member)) = node.member().resolve_to_definition() { + let Some(Definition::Enum(enum_definition)) = member.enclosing_definition() else { + unreachable!("an enum member is declared by an enum"); + }; + let ordinal = enum_definition + .members() + .iter() + .position(|candidate| candidate.node_id() == member.node_id()) + .expect("an enum lists the members it declares"); + let enum_type = self.typing(node.get_type()); + return Value::constant_from_bigint(&BigInt::from(ordinal), enum_type, self); + } match node.member().resolve_to_built_in() { Some(BuiltIn::AddressBalance) => Value::balance(self.expression(&node.operand()), self), Some(BuiltIn::AddressCodehash) => { @@ -43,6 +56,11 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' Some(BuiltIn::BlockBlobbasefee) => Value::block_blob_base_fee(self), Some(BuiltIn::BlockDifficulty) => Value::block_difficulty(self), Some(BuiltIn::BlockPrevrandao) => Value::block_prev_randao(self), + Some(BuiltIn::TypeEnumMin) => Value::zero(self.typing(node.get_type()), self), + Some(BuiltIn::TypeEnumMax) => { + let enum_type = self.typing(node.get_type()); + Value::constant_from_bigint(&BigInt::from(enum_type.enum_max()), enum_type, self) + } _ => unimplemented!("unsupported member access: {}", node.member().name()), } } diff --git a/solx-slang/src/contract/function/expression/unary.rs b/solx-slang/src/contract/function/expression/unary.rs index 0027188f0..dc076e3f0 100644 --- a/solx-slang/src/contract/function/expression/unary.rs +++ b/solx-slang/src/contract/function/expression/unary.rs @@ -7,6 +7,7 @@ use slang_solidity_v2::ast::PostfixExpression; use slang_solidity_v2::ast::PostfixExpressionOperator; use slang_solidity_v2::ast::PrefixExpression; use slang_solidity_v2::ast::PrefixExpressionOperator; +use slang_solidity_v2::ast::UserDefinedOperatorExpression; use solx_mlir::CmpPredicate; use solx_mlir::Context; @@ -18,6 +19,9 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// The prefix `++`, `--`, `~`, `!`, and `-` operators, each yielding a value; and `delete`, which /// resets its operand and yields nothing, `delete` being value-less in Solidity. pub fn prefix(&mut self, node: &PrefixExpression) -> Option> { + if let Some(function) = node.resolve_operator_to_function() { + return Some(self.bound_operator(&function, [node.operand()])); + } match node.operator() { PrefixExpressionOperator::PlusPlus(_) => Some(self.step(&node.operand(), Value::add).1), PrefixExpressionOperator::MinusMinus(_) => { diff --git a/solx-slang/src/contract/mod.rs b/solx-slang/src/contract/mod.rs index d0723ba04..79d26f38d 100644 --- a/solx-slang/src/contract/mod.rs +++ b/solx-slang/src/contract/mod.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use slang_solidity_v2::ast::ContractDefinition; use slang_solidity_v2::ast::ContractMember; +use slang_solidity_v2::ast::FunctionDefinition; use slang_solidity_v2::ast::FunctionKind; use solx_mlir::Block; @@ -26,12 +27,21 @@ impl<'context> SourceUnitScope<'context> { /// `method_identifiers` map (externally-dispatchable signature to 4-byte selector, lower-case /// hex); `convert-sol-to-yul` builds the entry-point dispatcher from the function selectors. /// Function signatures are pre-registered for call resolution before any body is emitted. - /// Inherited state variables are not yet declared: derived contracts do not compile through - /// this path. - pub fn contract_definition(&mut self, node: &ContractDefinition) -> BTreeMap { + /// `operator_functions` land in the contract body because MLIR has no file scope. Inherited + /// state variables are not yet declared: derived contracts do not compile through this path. + pub fn contract_definition( + &mut self, + node: &ContractDefinition, + operator_functions: &[FunctionDefinition], + ) -> BTreeMap { let contract_identifier = node.name(); - for function in node.functions().into_iter().chain(node.constructor()) { + for function in node + .functions() + .into_iter() + .chain(node.constructor()) + .chain(operator_functions.iter().cloned()) + { let parameter_types = function .parameters() .iter() @@ -108,8 +118,8 @@ impl<'context> SourceUnitScope<'context> { ); } scope.constructor(node); - for function in node.functions() { - scope.function_definition(&function); + for function in node.functions().iter().chain(operator_functions) { + scope.function_definition(function); } }, ); diff --git a/solx-slang/src/source_unit.rs b/solx-slang/src/source_unit.rs index 2bbcb48e3..a21b8d094 100644 --- a/solx-slang/src/source_unit.rs +++ b/solx-slang/src/source_unit.rs @@ -4,7 +4,12 @@ use std::collections::BTreeMap; +use itertools::Itertools; +use slang_solidity_v2::ast::Definition; +use slang_solidity_v2::ast::FunctionDefinition; use slang_solidity_v2::ast::SourceUnit; +use slang_solidity_v2::ast::SourceUnitMember; +use slang_solidity_v2::ast::UsingClause; use solx_mlir::Context; use solx_standard_json::output::contract::Contract; @@ -31,7 +36,8 @@ impl<'context> SourceUnitScope<'context> { }; let melior = Context::create_melior_context(); let mut scope = SourceUnitScope::new(Context::new(&melior, evm_version)); - let method_identifiers = scope.contract_definition(contract); + let method_identifiers = + scope.contract_definition(contract, &Self::operator_bound_functions(unit)); let name = contract.name().name().to_owned(); let mlir = Context::from(scope).finalize_module( @@ -44,4 +50,30 @@ impl<'context> SourceUnitScope<'context> { Contract::new_mlir(mlir, method_identifiers), )])) } + + /// The functions `unit`'s `using {f as op} for T global;` directives bind operators to, in + /// source order and without repeats. A binding reaches the whole compilation unit, so a + /// function bound from another file is missing here. + // TODO: add support for cross-file binding of operator functions + fn operator_bound_functions(unit: &SourceUnit) -> Vec { + unit.members() + .iter() + .filter_map(|member| match member { + SourceUnitMember::UsingDirective(directive) if directive.is_global() => { + match directive.clause() { + UsingClause::UsingDeconstruction(deconstruction) => Some(deconstruction), + UsingClause::IdentifierPath(_) => None, + } + } + _ => None, + }) + .flat_map(|deconstruction| deconstruction.symbols().iter().collect::>()) + .filter(|symbol| symbol.alias().is_some()) + .filter_map(|symbol| match symbol.name().resolve_to_definition()? { + Definition::Function(function) => Some(function), + _ => None, + }) + .unique_by(|function| function.node_id()) + .collect() + } } From 21320b77a379f43e591a8581182d48d590816e50 Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Thu, 30 Jul 2026 21:18:33 +0400 Subject: [PATCH 04/11] fix(mlir): destroy the runtime module detached from the Sol pass output (#624) --- solx-mlir/src/context/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solx-mlir/src/context/mod.rs b/solx-mlir/src/context/mod.rs index b34008c90..0a0593884 100644 --- a/solx-mlir/src/context/mod.rs +++ b/solx-mlir/src/context/mod.rs @@ -14,6 +14,7 @@ use melior::ir::AttributeLike; use melior::ir::BlockLike; use melior::ir::Location; use melior::ir::Module; +use melior::ir::Operation; use melior::ir::attribute::StringAttribute; use melior::ir::operation::OperationLike; use melior::ir::operation::OperationMutLike; @@ -243,8 +244,7 @@ impl<'context> Context<'context> { } /// Finds a nested `builtin.module` in `module`'s body whose `sym_name` - /// matches `target`, removes it from the parent, and returns its - /// textual form. + /// matches `target`, destroys it, and returns its textual form. fn take_nested_module_text(module: &mut Module, target: &str) -> anyhow::Result { let body = module.body(); std::iter::successors(body.first_operation_mut(), |operation| { @@ -261,6 +261,7 @@ impl<'context> Context<'context> { } let text = operation.to_string(); operation.remove_from_parent(); + drop(unsafe { Operation::from_raw(operation.to_raw()) }); Some(text) }) .ok_or_else(|| anyhow::anyhow!("no module with sym_name `{target}` in Sol pass output")) From 7fc1212567f63491fcd2439d80072136b469c2fd Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Fri, 31 Jul 2026 15:07:24 +0400 Subject: [PATCH 05/11] chore(deps): source tblgen from NomicFoundation (#627) --- Cargo.lock | 6 +++--- deny.toml | 2 +- solx-mlir/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bf56811d..8e58b1934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2776,7 +2776,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "melior" version = "0.26.9" -source = "git+https://github.com/NomicFoundation/melior?rev=1e6f01701b344bec2458ac938b661db669cf27a2#1e6f01701b344bec2458ac938b661db669cf27a2" +source = "git+https://github.com/NomicFoundation/melior?rev=d188691b0620f91ff83e98cdbdf60507b2ee37d9#d188691b0620f91ff83e98cdbdf60507b2ee37d9" dependencies = [ "melior-macro", "mlir-sys", @@ -2785,7 +2785,7 @@ dependencies = [ [[package]] name = "melior-macro" version = "0.19.5" -source = "git+https://github.com/NomicFoundation/melior?rev=1e6f01701b344bec2458ac938b661db669cf27a2#1e6f01701b344bec2458ac938b661db669cf27a2" +source = "git+https://github.com/NomicFoundation/melior?rev=d188691b0620f91ff83e98cdbdf60507b2ee37d9#d188691b0620f91ff83e98cdbdf60507b2ee37d9" dependencies = [ "comrak", "convert_case 0.11.0", @@ -5128,7 +5128,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tblgen" version = "0.8.1" -source = "git+https://github.com/hedgar2017/tblgen-rs?rev=56ad078a9bfc3ee785d56ef63bb923425ce1e088#56ad078a9bfc3ee785d56ef63bb923425ce1e088" +source = "git+https://github.com/NomicFoundation/tblgen-rs?rev=56ad078a9bfc3ee785d56ef63bb923425ce1e088#56ad078a9bfc3ee785d56ef63bb923425ce1e088" dependencies = [ "bindgen", "cc", diff --git a/deny.toml b/deny.toml index 200d19055..c3aff6248 100644 --- a/deny.toml +++ b/deny.toml @@ -79,7 +79,7 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"] allow-git = [ "https://github.com/NomicFoundation/inkwell", "https://github.com/NomicFoundation/llvm-sys.rs", + "https://github.com/NomicFoundation/tblgen-rs", "https://github.com/NomicFoundation/melior", "https://github.com/NomicFoundation/slang", - "https://github.com/hedgar2017/tblgen-rs", ] diff --git a/solx-mlir/Cargo.toml b/solx-mlir/Cargo.toml index f4e58fb0c..a4ae1250e 100644 --- a/solx-mlir/Cargo.toml +++ b/solx-mlir/Cargo.toml @@ -13,7 +13,7 @@ cc = "1.2.60" anyhow.workspace = true clap.workspace = true num.workspace = true -melior = { git = "https://github.com/NomicFoundation/melior", rev = "1e6f01701b344bec2458ac938b661db669cf27a2", features = ["ods-dialects", "helpers"] } +melior = { git = "https://github.com/NomicFoundation/melior", rev = "d188691b0620f91ff83e98cdbdf60507b2ee37d9", features = ["ods-dialects", "helpers"] } serde.workspace = true # mlir-sys 210 wraps LLVM 21.0 C API; llvm-sys (via inkwell) targets 21.1. # Both are built from the same LLVM source tree (LLVM_SYS_211_PREFIX == From caa5ed1de95045ec8866fd1fab8c1760b97b85df Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Fri, 31 Jul 2026 17:36:19 +0400 Subject: [PATCH 06/11] feat(slang): emit every contract in a source unit (#460) --- solx-mlir/tests/lit/module.sol | 32 ++++++++++++++++++++++ solx-slang/src/contract/mod.rs | 3 +-- solx-slang/src/source_unit.rs | 49 ++++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/solx-mlir/tests/lit/module.sol b/solx-mlir/tests/lit/module.sol index 21f60b72d..1f6c216d0 100644 --- a/solx-mlir/tests/lit/module.sol +++ b/solx-mlir/tests/lit/module.sol @@ -11,8 +11,40 @@ // CHECK-NEXT: sol.return %{{.*}} : ui256 // CHECK: } {kind = #Contract} +// CHECK: module attributes {llvm.data_layout = "E-p:256:256-i256:256:256-S256-a:256:256", llvm.target_triple = "evm-unknown-unknown" +// CHECK: sol.contract @{{.*Impl.*}} { +// CHECK: sol.func @{{.*h.*}}() -> ui256 +// CHECK: } {kind = #Contract} + +// CHECK: module attributes {llvm.data_layout = "E-p:256:256-i256:256:256-S256-a:256:256", llvm.target_triple = "evm-unknown-unknown" +// CHECK: sol.contract @{{.*Second.*}} { +// CHECK: sol.func @{{.*g.*}}() -> ui256 +// CHECK: } {kind = #Contract} + +// CHECK-NOT: sol.contract + contract C { function f() public pure returns (uint256) { return 42; } } + +interface Iface { + function h() external pure returns (uint256); +} + +contract Impl is Iface { + function h() external pure returns (uint256) { + return 1; + } +} + +contract Second { + function g() public pure returns (uint256) { + return 7; + } +} + +abstract contract Undeployable { + function h() public pure virtual returns (uint256); +} diff --git a/solx-slang/src/contract/mod.rs b/solx-slang/src/contract/mod.rs index 79d26f38d..9fba37612 100644 --- a/solx-slang/src/contract/mod.rs +++ b/solx-slang/src/contract/mod.rs @@ -27,8 +27,7 @@ impl<'context> SourceUnitScope<'context> { /// `method_identifiers` map (externally-dispatchable signature to 4-byte selector, lower-case /// hex); `convert-sol-to-yul` builds the entry-point dispatcher from the function selectors. /// Function signatures are pre-registered for call resolution before any body is emitted. - /// `operator_functions` land in the contract body because MLIR has no file scope. Inherited - /// state variables are not yet declared: derived contracts do not compile through this path. + /// `operator_functions` land in the contract body because MLIR has no file scope. pub fn contract_definition( &mut self, node: &ContractDefinition, diff --git a/solx-slang/src/source_unit.rs b/solx-slang/src/source_unit.rs index a21b8d094..86e39f3d2 100644 --- a/solx-slang/src/source_unit.rs +++ b/solx-slang/src/source_unit.rs @@ -5,6 +5,7 @@ use std::collections::BTreeMap; use itertools::Itertools; +use slang_solidity_v2::ast::ContractBase; use slang_solidity_v2::ast::Definition; use slang_solidity_v2::ast::FunctionDefinition; use slang_solidity_v2::ast::SourceUnit; @@ -18,37 +19,45 @@ use solx_utils::EVMVersion; use crate::scope::source_unit::SourceUnitScope; impl<'context> SourceUnitScope<'context> { - /// Lowers the unit's contracts, owning the per-file melior scope, into standard-JSON contract - /// outputs keyed by contract name. Only the first contract per file compiles through this path - /// today; the rest are skipped until inheritance-aware emission lands. + /// Lowers every contract the unit deploys into standard-JSON contract outputs keyed by contract + /// name, each in its own MLIR module off the file's melior context, as solc emits one module per + /// contract. An abstract contract and an interface deploy nothing and produce no module. A + /// contract with a contract base is skipped because emission collects only the contract's own + /// state and functions: an interface base carries nothing to inherit, a contract base carries + /// state and bodies that would be silently dropped. /// /// # Errors /// /// Returns an error if module finalization fails. + // TODO: emit a contract inheriting from a contract, which needs its inherited state variables + // and functions, and a library, whose own address reaches LLVM through the untranslated + // `llvm.setimmutable`. pub fn source_unit( unit: &SourceUnit, evm_version: EVMVersion, capture_sol_dialect: impl Fn(&str) -> bool, ) -> anyhow::Result> { - let contracts = unit.contracts(); - let Some(contract) = contracts.first() else { - return Ok(BTreeMap::new()); - }; + let operator_functions = Self::operator_bound_functions(unit); let melior = Context::create_melior_context(); - let mut scope = SourceUnitScope::new(Context::new(&melior, evm_version)); - let method_identifiers = - scope.contract_definition(contract, &Self::operator_bound_functions(unit)); + let mut contracts = BTreeMap::new(); + for contract in unit.contracts().iter().filter(|contract| { + !contract.is_abstract() + && !contract + .direct_bases() + .iter() + .any(|base| matches!(base, ContractBase::Contract(_))) + }) { + let mut scope = SourceUnitScope::new(Context::new(&melior, evm_version)); + let method_identifiers = scope.contract_definition(contract, &operator_functions); - let name = contract.name().name().to_owned(); - let mlir = Context::from(scope).finalize_module( - &format!("{name}{}", solx_codegen_evm::DEPLOYED_OBJECT_SUFFIX), - capture_sol_dialect(&name), - )?; - - Ok(BTreeMap::from([( - name, - Contract::new_mlir(mlir, method_identifiers), - )])) + let name = contract.name().name().to_owned(); + let mlir = Context::from(scope).finalize_module( + &format!("{name}{}", solx_codegen_evm::DEPLOYED_OBJECT_SUFFIX), + capture_sol_dialect(&name), + )?; + contracts.insert(name, Contract::new_mlir(mlir, method_identifiers)); + } + Ok(contracts) } /// The functions `unit`'s `using {f as op} for T global;` directives bind operators to, in From ac7bab37d04b6a545389a82b273837315994437d Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Sat, 1 Aug 2026 01:34:56 +0400 Subject: [PATCH 07/11] feat(slang): internal function pointers (#620) --- Cargo.lock | 1 + solx-core/Cargo.toml | 2 +- solx-mlir/Cargo.toml | 8 +- solx-mlir/sol_attr_stubs.cpp | 11 ++ solx-mlir/src/context/function/dispatch.rs | 31 +++++ .../context/{function.rs => function/mod.rs} | 101 ++++++-------- solx-mlir/src/ffi.rs | 10 ++ solx-mlir/src/ir/attributes.rs | 12 ++ solx-mlir/src/ir/mod.rs | 14 ++ solx-mlir/src/ir/type/function_type.rs | 48 +++++++ solx-mlir/src/ir/type/mod.rs | 6 + solx-mlir/src/ir/value.rs | 11 +- solx-mlir/src/lib.rs | 2 + solx-mlir/src/macros.rs | 25 ++-- solx-mlir/tests/lit/address_code.sol | 12 +- .../tests/lit/delete_function_pointer.sol | 23 ++++ solx-mlir/tests/lit/evm_context.sol | 5 +- .../tests/lit/internal_function_pointer.sol | 129 ++++++++++++++++++ solx-mlir/tests/lit/value_transfer.sol | 11 +- solx-slang/Cargo.toml | 3 +- .../contract/function/expression/call/mod.rs | 67 ++++++--- .../function/expression/identifier.rs | 23 +++- .../contract/function/expression/member.rs | 28 ++-- solx-slang/src/contract/function/mod.rs | 35 ++--- solx-slang/src/contract/mod.rs | 21 +-- solx-slang/src/source_unit.rs | 8 +- solx-slang/src/type.rs | 32 +++++ solx-utils/Cargo.toml | 6 +- solx-utils/src/data_location.rs | 1 - solx-utils/src/evm_version.rs | 1 - solx-utils/src/extension.rs | 1 - 31 files changed, 519 insertions(+), 169 deletions(-) create mode 100644 solx-mlir/src/context/function/dispatch.rs rename solx-mlir/src/context/{function.rs => function/mod.rs} (50%) create mode 100644 solx-mlir/src/ir/type/function_type.rs create mode 100644 solx-mlir/tests/lit/delete_function_pointer.sol create mode 100644 solx-mlir/tests/lit/internal_function_pointer.sol diff --git a/Cargo.lock b/Cargo.lock index 8e58b1934..79a2941cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4859,6 +4859,7 @@ dependencies = [ "num", "ruint", "serde", + "slang_solidity_v2", "solx-utils", ] diff --git a/solx-core/Cargo.toml b/solx-core/Cargo.toml index d76268d92..31d6baefd 100644 --- a/solx-core/Cargo.toml +++ b/solx-core/Cargo.toml @@ -34,4 +34,4 @@ solx-utils = { path = "../solx-utils" } [features] solc = [] -mlir = ["dep:solx-mlir", "solx-standard-json/mlir", "solx-utils/mlir"] +mlir = ["dep:solx-mlir", "solx-standard-json/mlir"] diff --git a/solx-mlir/Cargo.toml b/solx-mlir/Cargo.toml index a4ae1250e..e2c454569 100644 --- a/solx-mlir/Cargo.toml +++ b/solx-mlir/Cargo.toml @@ -13,6 +13,9 @@ cc = "1.2.60" anyhow.workspace = true clap.workspace = true num.workspace = true +ruint.workspace = true + +inkwell = { workspace = true, features = ["llvm21-1-no-llvm-linking"] } melior = { git = "https://github.com/NomicFoundation/melior", rev = "d188691b0620f91ff83e98cdbdf60507b2ee37d9", features = ["ods-dialects", "helpers"] } serde.workspace = true # mlir-sys 210 wraps LLVM 21.0 C API; llvm-sys (via inkwell) targets 21.1. @@ -23,10 +26,9 @@ serde.workspace = true # TODO: Track mlir-sys 211 availability. mlir-sys = "210.0" -inkwell = { workspace = true, features = ["llvm21-1-no-llvm-linking"] } +slang_solidity_v2.workspace = true -ruint.workspace = true -solx-utils = { path = "../solx-utils", features = ["mlir"] } +solx-utils = { path = "../solx-utils" } [dev-dependencies] hex.workspace = true diff --git a/solx-mlir/sol_attr_stubs.cpp b/solx-mlir/sol_attr_stubs.cpp index 049477122..54c57cefa 100644 --- a/solx-mlir/sol_attr_stubs.cpp +++ b/solx-mlir/sol_attr_stubs.cpp @@ -10,6 +10,7 @@ */ #include "mlir/Dialect/Sol/Sol.h" +#include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/MLIRContext.h" #include "mlir-c/BuiltinAttributes.h" #include "mlir-c/IR.h" @@ -133,6 +134,12 @@ MlirType solxCreateEnumType(MlirContext ctx, uint32_t max) { return wrap(mlir::sol::EnumType::get(context, max)); } +MlirType solxCreateFuncRefType(MlirContext ctx, MlirType signature) { + auto *context = unwrap(ctx); + auto functionType = mlir::cast(unwrap(signature)); + return wrap(mlir::sol::FuncRefType::get(context, functionType)); +} + bool solxIsAddressType(MlirType ty) { return mlir::isa(unwrap(ty)); } @@ -157,6 +164,10 @@ uint32_t solxBytesLikeTypeWidth(MlirType ty) { return mlir::sol::getNumBytes(unwrap(ty)); } +bool solxIsFuncRefType(MlirType ty) { + return mlir::isa(unwrap(ty)); +} + bool solxIsScalarType(MlirType ty) { return mlir::sol::isScalar(unwrap(ty)); } diff --git a/solx-mlir/src/context/function/dispatch.rs b/solx-mlir/src/context/function/dispatch.rs new file mode 100644 index 000000000..a9886c97a --- /dev/null +++ b/solx-mlir/src/context/function/dispatch.rs @@ -0,0 +1,31 @@ +//! +//! The attribute a `sol.func` is reached through. +//! + +use slang_solidity_v2::ast::FunctionDefinition; +use slang_solidity_v2::ast::FunctionKind as SlangFunctionKind; +use slang_solidity_v2::ast::NodeId; + +use crate::FunctionKind; + +/// The attribute a `sol.func` is reached through. A dispatch identifier is never zero, which the +/// dialect reserves for the null function pointer, since slang numbers nodes from one. +#[derive(Clone, Copy)] +pub enum FunctionDispatch { + /// The identifier an internal function pointer dispatches to. + Identifier(NodeId), + /// The dialect kind of a constructor, fallback or receive function. + Kind(FunctionKind), +} + +impl From<&FunctionDefinition> for FunctionDispatch { + fn from(function: &FunctionDefinition) -> Self { + match function.kind() { + SlangFunctionKind::Constructor => Self::Kind(FunctionKind::Constructor), + SlangFunctionKind::Fallback => Self::Kind(FunctionKind::Fallback), + SlangFunctionKind::Receive => Self::Kind(FunctionKind::Receive), + SlangFunctionKind::Regular => Self::Identifier(function.node_id()), + SlangFunctionKind::Modifier => unreachable!("slang yields no modifier as a function"), + } + } +} diff --git a/solx-mlir/src/context/function.rs b/solx-mlir/src/context/function/mod.rs similarity index 50% rename from solx-mlir/src/context/function.rs rename to solx-mlir/src/context/function/mod.rs index 578bdb95f..6f2b0fdc9 100644 --- a/solx-mlir/src/context/function.rs +++ b/solx-mlir/src/context/function/mod.rs @@ -1,25 +1,26 @@ //! -//! Function call resolution metadata, and the `sol.func` / `sol.call` it emits. +//! Function call resolution metadata. //! +pub mod dispatch; + use melior::ir::Block as MlirBlock; use melior::ir::Region; use melior::ir::RegionLike; -use melior::ir::attribute::FlatSymbolRefAttribute; use melior::ir::attribute::IntegerAttribute; use melior::ir::attribute::StringAttribute; use melior::ir::attribute::TypeAttribute; use melior::ir::operation::OperationLike; -use melior::ir::r#type::FunctionType; use melior::ir::r#type::IntegerType; use crate::Block; use crate::Context; +use crate::FunctionDispatch; use crate::FunctionKind; +use crate::FunctionType; use crate::StateMutability; use crate::Type; use crate::Value; -use crate::ods::sol::CallOperation; use crate::ods::sol::FuncOperation; /// Cached signature of a lowered function: its mangled symbol and MLIR-interned parameter and @@ -28,10 +29,8 @@ use crate::ods::sol::FuncOperation; pub struct Function<'context> { /// The mangled MLIR function name. pub mlir_name: String, - /// Parameter types, MLIR-interned, exact from the function signature. - pub parameter_types: Vec>, - /// Return types, MLIR-interned, exact from the function signature. - pub return_types: Vec>, + /// Parameter and result types, MLIR-interned, exact from the function signature. + pub function_type: FunctionType<'context>, } impl<'context> Function<'context> { @@ -39,51 +38,41 @@ impl<'context> Function<'context> { pub const CONSTRUCTOR_NAME: &'static str = "@constructor()"; /// Records a function's mangled name and interned signature. - pub fn new( - mlir_name: String, - parameter_types: Vec>, - return_types: Vec>, - ) -> Self { + pub fn new(mlir_name: String, function_type: FunctionType<'context>) -> Self { Self { mlir_name, - parameter_types, - return_types, + function_type, } } /// The signature of a synthesized parameterless constructor. pub fn constructor() -> Self { - Self::new(Self::CONSTRUCTOR_NAME.to_owned(), Vec::new(), Vec::new()) + Self::new(Self::CONSTRUCTOR_NAME.to_owned(), FunctionType::default()) } /// Emits this function's `sol.func` definition with an entry block whose arguments carry the - /// parameter types, returned for the body. `selector` / `kind` are the optional dispatch - /// attributes; an original function type is attached for selector-dispatched and constructor - /// functions. + /// parameter types, returned for the body. An original function type is attached for + /// selector-dispatched and constructor functions. pub fn define( &self, selector: Option, + dispatch: FunctionDispatch, state_mutability: StateMutability, - kind: Option, context: &Context<'context>, contract_body: Block<'context>, ) -> Block<'context> { - let parameter_types = self - .parameter_types - .iter() - .map(|parameter_type| parameter_type.into_mlir()) - .collect::>(); - let return_types = self - .return_types + let parameters = self + .function_type + .parameters .iter() - .map(|return_type| return_type.into_mlir()) + .map(|parameter| parameter.into_mlir()) .collect::>(); - let function_type = FunctionType::new(context.melior, ¶meter_types, &return_types); + let function_type = self.function_type.to_mlir(context.melior); let body_region = Region::new(); let entry_block = MlirBlock::new( - ¶meter_types + ¶meters .iter() - .map(|parameter_type| (*parameter_type, context.location())) + .map(|parameter| (*parameter, context.location())) .collect::>(), ); body_region.append_block(entry_block); @@ -93,16 +82,26 @@ impl<'context> Function<'context> { .function_type(TypeAttribute::new(function_type.into())) .state_mutability(state_mutability.attribute(context.melior)) .body(body_region); - if let Some(function_kind) = kind { - operation_builder = operation_builder.kind(function_kind.attribute(context.melior)); - } + operation_builder = match dispatch { + FunctionDispatch::Identifier(identifier) => { + operation_builder.id(IntegerAttribute::new( + IntegerType::new(context.melior, solx_utils::BIT_LENGTH_X64 as u32).into(), + usize::from(identifier) as i64, + )) + } + FunctionDispatch::Kind(function_kind) => { + operation_builder.kind(function_kind.attribute(context.melior)) + } + }; if let Some(selector_value) = selector { operation_builder = operation_builder.selector(IntegerAttribute::new( IntegerType::new(context.melior, Type::SELECTOR_BIT_WIDTH).into(), selector_value as i64, )); } - if selector.is_some() || matches!(kind, Some(FunctionKind::Constructor)) { + if selector.is_some() + || matches!(dispatch, FunctionDispatch::Kind(FunctionKind::Constructor)) + { operation_builder = operation_builder.orig_fn_type(TypeAttribute::new(function_type.into())); } @@ -116,32 +115,12 @@ impl<'context> Function<'context> { ) } - /// Emits a `sol.call` to `callee` by symbol, returning its results in declaration order. - pub fn call( - callee: &str, - operands: &[Value<'context>], - result_types: &[Type<'context>], - context: &Context<'context>, - ) -> anyhow::Result>> { - let operands = operands - .iter() - .map(|operand| operand.into_mlir()) - .collect::>(); - let result_types = result_types - .iter() - .map(|result_type| result_type.into_mlir()) - .collect::>(); - let operation = context.current_block().append_operation(mlir_op_build!( + /// Emits the internal function pointer to this function (`sol.func_constant`). + pub fn pointer_constant(&self, context: &Context<'context>) -> Value<'context> { + Value::function_constant( + &self.mlir_name, + self.function_type.reference(context.melior), context, - CallOperation - .callee(FlatSymbolRefAttribute::new(context.melior, callee)) - .outs(result_types.as_slice()) - .operands(operands.as_slice()) - )); - let mut results = Vec::with_capacity(result_types.len()); - for index in 0..result_types.len() { - results.push(Value::from(operation.result(index)?)); - } - Ok(results) + ) } } diff --git a/solx-mlir/src/ffi.rs b/solx-mlir/src/ffi.rs index f08905879..2d31d7354 100644 --- a/solx-mlir/src/ffi.rs +++ b/solx-mlir/src/ffi.rs @@ -157,6 +157,13 @@ unsafe extern "C" { /// (one less than the number of enum members). pub fn solxCreateEnumType(context: MlirContext, max: u32) -> mlir_sys::MlirType; + /// Creates a `sol::FuncRefType`, an internal function pointer, over `signature`, a + /// `mlir::FunctionType`. + pub fn solxCreateFuncRefType( + context: MlirContext, + signature: mlir_sys::MlirType, + ) -> mlir_sys::MlirType; + // ---- Sol type inference ---- /// Whether the type is a `sol::AddressType`, regardless of payability. @@ -178,6 +185,9 @@ unsafe extern "C" { /// Returns the byte width of a bytes-like type. pub fn solxBytesLikeTypeWidth(ty: mlir_sys::MlirType) -> u32; + /// Whether the type is a `sol::FuncRefType`, an internal function pointer. + pub fn solxIsFuncRefType(ty: mlir_sys::MlirType) -> bool; + /// Whether the type is a scalar value type: integer, enum, function reference, /// address-like, or bytes-like. pub fn solxIsScalarType(ty: mlir_sys::MlirType) -> bool; diff --git a/solx-mlir/src/ir/attributes.rs b/solx-mlir/src/ir/attributes.rs index 86b2ac56f..521bf19a9 100644 --- a/solx-mlir/src/ir/attributes.rs +++ b/solx-mlir/src/ir/attributes.rs @@ -42,6 +42,18 @@ sol_dialect_attribute! { } } +impl From for StateMutability { + fn from(mutability: slang_solidity_v2::ast::FunctionMutability) -> Self { + use slang_solidity_v2::ast::FunctionMutability as Slang; + match mutability { + Slang::Pure => Self::Pure, + Slang::View => Self::View, + Slang::Payable => Self::Payable, + Slang::NonPayable => Self::NonPayable, + } + } +} + sol_predicate_attribute! { /// Sol dialect `sol.cmp` predicate values. Signedness is carried by the operand type, not the /// predicate. diff --git a/solx-mlir/src/ir/mod.rs b/solx-mlir/src/ir/mod.rs index bb675838a..6e5255b45 100644 --- a/solx-mlir/src/ir/mod.rs +++ b/solx-mlir/src/ir/mod.rs @@ -12,6 +12,7 @@ pub mod value; use melior::ir::BlockLike; use crate::Block; +use crate::Function; use crate::Place; use crate::Value; use crate::ods::sol::*; @@ -26,6 +27,12 @@ sol_ops! { Value::array_literal(elements: values, array_type: ty) -> value { ArrayLitOperation.ins(many(elements)).addr(array_type) } + Value::function_constant(symbol: str, result_type: ty) -> value { + FuncConstantOperation.sym(symbol_attr(symbol)).addr(result_type) + } + Value::default_function_constant(result_type: ty) -> value { + DefaultFuncConstantOperation.addr(result_type) + } Value::cast(self, target_type: ty) -> value nop_if_same(target_type) { CastOperation.inp(self).out(target_type) @@ -169,6 +176,13 @@ sol_ops! { Value::gas_left() -> value { GasLeftOperation.val(field()) } Value::this(contract_type: ty) -> value { ThisOperation.addr(contract_type) } + Function::call(callee: str, operands: values, result_types: types) -> values { + CallOperation.callee(symbol_attr(callee)).outs(many(result_types)).operands(many(operands)) + } + Value::indirect_call(self, operands: values, result_types: types) -> values { + ICallOperation.callee(self).outs(many(result_types)).callee_operands(many(operands)) + } + Place::stack(pointee: ty) -> place { AllocaOperation.alloc_type(ty_attr(ptr(pointee, stack))).addr(ptr(pointee, stack)) } diff --git a/solx-mlir/src/ir/type/function_type.rs b/solx-mlir/src/ir/type/function_type.rs new file mode 100644 index 000000000..5eafac7a4 --- /dev/null +++ b/solx-mlir/src/ir/type/function_type.rs @@ -0,0 +1,48 @@ +//! +//! Signature parameter for the Sol function reference types. +//! + +use melior::ir::Type as MlirType; +use melior::ir::TypeLike; +use melior::ir::r#type::FunctionType as MlirFunctionType; + +use crate::Type; +use crate::ffi; + +/// The types a function is called through: what a `sol::FuncRefType` is parameterized by, and what +/// a call names as its callee type. The zero-argument default is the unit signature `() -> ()`. +#[derive(Default, Clone)] +pub struct FunctionType<'context> { + /// Parameter types, in declaration order. + pub parameters: Vec>, + /// Result types, one per returned value: the dialect has no tuple. + pub results: Vec>, +} + +impl<'context> FunctionType<'context> { + /// The melior function type this signature interns to: what a `sol.func` declares and what a + /// function reference is parameterized by. + pub fn to_mlir(&self, context: &'context melior::Context) -> MlirFunctionType<'context> { + let parameters: Vec> = self + .parameters + .iter() + .map(|parameter| parameter.into_mlir()) + .collect(); + let results: Vec> = self + .results + .iter() + .map(|result| result.into_mlir()) + .collect(); + MlirFunctionType::new(context, ¶meters, &results) + } + + /// A `sol::FuncRefType` over this signature, an internal function pointer. + pub fn reference(&self, context: &'context melior::Context) -> Type<'context> { + Type::new(unsafe { + MlirType::from_raw(ffi::solxCreateFuncRefType( + context.to_raw(), + MlirType::from(self.to_mlir(context)).to_raw(), + )) + }) + } +} diff --git a/solx-mlir/src/ir/type/mod.rs b/solx-mlir/src/ir/type/mod.rs index efa5ea53e..1f573f8a9 100644 --- a/solx-mlir/src/ir/type/mod.rs +++ b/solx-mlir/src/ir/type/mod.rs @@ -3,6 +3,7 @@ //! pub mod array_size; +pub mod function_type; use std::ffi::c_char; @@ -226,6 +227,11 @@ impl<'context> Type<'context> { unsafe { ffi::solxBytesLikeTypeWidth(self.inner.to_raw()) } } + /// Whether this is a `sol::FuncRefType`, an internal function pointer. + pub fn is_function_reference(self) -> bool { + unsafe { ffi::solxIsFuncRefType(self.inner.to_raw()) } + } + /// Whether this is a scalar value type: integer, enum, function reference, address-like, or /// bytes-like. pub fn is_scalar(self) -> bool { diff --git a/solx-mlir/src/ir/value.rs b/solx-mlir/src/ir/value.rs index 7c08bc82c..7878d579b 100644 --- a/solx-mlir/src/ir/value.rs +++ b/solx-mlir/src/ir/value.rs @@ -69,11 +69,14 @@ impl<'context> Value<'context> { Self::constant(i64::from(value), Type::boolean(context.melior), context) } - /// Materialises the default-initialized value of `target_type`, matching solc's default-init: - /// a scalar's `zero`; an allocated empty memory `bytes`/`string`; a zero-filled memory array - /// or struct; the `sol.default_storage` / `sol.default_calldata` designator for a storage or - /// calldata reference. + /// Materialises the default-initialized value of `target_type`, matching solc's default-init. + /// A function reference cannot route through `zero`, since `sol.constant` is illegal at a + /// `!sol.func_ref`, and a reference type's default is its location's designator rather than a + /// value. pub fn default_initialized(target_type: Type<'context>, context: &Context<'context>) -> Self { + if target_type.is_function_reference() { + return Self::default_function_constant(target_type, context); + } if target_type.is_scalar() { return Self::zero(target_type, context); } diff --git a/solx-mlir/src/lib.rs b/solx-mlir/src/lib.rs index 02a0269e2..e3b2c5c5e 100644 --- a/solx-mlir/src/lib.rs +++ b/solx-mlir/src/lib.rs @@ -24,6 +24,7 @@ pub use self::context::Context; pub use self::context::contract::Contract; pub use self::context::environment::Environment; pub use self::context::function::Function; +pub use self::context::function::dispatch::FunctionDispatch; pub use self::dialect::Dialect; pub use self::ir::attributes::CmpPredicate; pub use self::ir::attributes::ContractKind; @@ -33,6 +34,7 @@ pub use self::ir::block::Block; pub use self::ir::place::Place; pub use self::ir::r#type::Type; pub use self::ir::r#type::array_size::ArraySize; +pub use self::ir::r#type::function_type::FunctionType; pub use self::ir::value::Value; pub use self::macros::IntoOds; pub use self::output::MlirOutput; diff --git a/solx-mlir/src/macros.rs b/solx-mlir/src/macros.rs index 40b180073..f8db7d5ff 100644 --- a/solx-mlir/src/macros.rs +++ b/solx-mlir/src/macros.rs @@ -169,17 +169,13 @@ impl<'slice, T, const N: usize> IntoOds<&'slice [T]> for &'slice [T; N] { } } -/// Declares Sol dialect op-wrapper methods on [`Value`](crate::Value), [`Place`](crate::Place), and -/// [`Block`](crate::Block) as pure data: one ODS operation per declaration. +/// Declares Sol dialect op-wrapper methods on their entity homes as pure data: one ODS operation +/// per declaration. /// /// A declaration names the receiver, the method and its typed parameters, the disposition, the /// operation, and the builder setter chain. Every setter argument is a parameter, the receiver -/// `self`, or a closed keyword. Keywords are call-shaped, so a bare identifier is always a parameter: -/// result types `field()` / `address()` / `boolean()` / `memory()` / `calldata()` / `fixed_bytes(N)` -/// / `ptr(pointee, stack)`; the receiver-derived `self` / `self_ty` / `gep_of(elem)`; attributes -/// `int_attr` / `str_attr` / `bytes_attr` / `symbol_attr` / `predicate_attr` / `ty_attr` / `count_attr`; variadic -/// operands `single` / `many` / `concat`; conditional setters `optional_str` / `optional_value`; the -/// always-set unit flag `unit_flag`. The operation slot may be `checked(CheckedOp, UncheckedOp)`, +/// `self`, or a closed keyword from the `@arg` rules; keywords are call-shaped, so a bare +/// identifier is always a parameter. The operation slot may be `checked(CheckedOp, UncheckedOp)`, /// which threads a `checked: bool` selector. /// /// A `base | flagged (…) … { … } flagged .setter ;` declaration stamps a pair of methods off one @@ -201,6 +197,7 @@ macro_rules! sol_ops { (@ty value) => { $crate::Value<'context> }; (@ty values) => { &[$crate::Value<'context>] }; (@ty ty) => { $crate::Type<'context> }; + (@ty types) => { &[$crate::Type<'context>] }; (@ty predicate) => { $crate::CmpPredicate }; (@ty optional_str) => { ::core::option::Option<&str> }; (@ty optional_value) => { ::core::option::Option<$crate::Value<'context>> }; @@ -319,6 +316,7 @@ macro_rules! sol_ops { (@disp_ty value) => { $crate::Value<'context> }; (@disp_ty place) => { $crate::Place<'context> }; + (@disp_ty values) => { ::std::vec::Vec<$crate::Value<'context>> }; (@region_tuple $region:ident ; empty $($empty_region:ident),+) => { $crate::Block<'context> @@ -343,6 +341,17 @@ macro_rules! sol_ops { (@emit place [$context:ident] $operation:expr, $message:expr) => { $crate::Place::from(sol_ops!(@one_result [$context] $operation, $message)) }; + (@emit values [$context:ident] $operation:expr, $message:expr) => {{ + let operation = $context.current_block().append_operation($operation); + (0..::melior::ir::operation::OperationLike::result_count(&operation)) + .map(|index| { + $crate::Value::from( + ::melior::ir::operation::OperationLike::result(&operation, index) + .expect("the index is bounded by the result count"), + ) + }) + .collect::<::std::vec::Vec<_>>() + }}; ( $receiver:ident :: $base:ident | $flagged:ident ($($argument:ident : $kind:ident),* $(,)?) diff --git a/solx-mlir/tests/lit/address_code.sol b/solx-mlir/tests/lit/address_code.sol index 4728e0be9..c8efc47e6 100644 --- a/solx-mlir/tests/lit/address_code.sol +++ b/solx-mlir/tests/lit/address_code.sol @@ -1,8 +1,16 @@ // RUN: solx --emit-mlir=sol %s | FileCheck %s // RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s -// CHECK: sol.code {{.*}} : !sol.address -> !sol.string +// CHECK: sol.func @{{.*bytecode.*}} +// CHECK: %[[CODE_RECEIVER:.*]] = sol.address_cast %{{.*}} : !sol.address to !sol.address +// CHECK: sol.code %[[CODE_RECEIVER]] : !sol.address -> !sol.string + +// CHECK: sol.func @{{.*bytecode_hash.*}} +// CHECK: %[[HASH_RECEIVER:.*]] = sol.address_cast %{{.*}} : !sol.address to !sol.address +// CHECK: sol.code_hash %[[HASH_RECEIVER]] : !sol.address -> ui256 contract C { - function bytecode(address a) public view returns (bytes memory) { return a.code; } + function bytecode(address payable a) public view returns (bytes memory) { return a.code; } + + function bytecode_hash(address payable a) public view returns (bytes32) { return a.codehash; } } diff --git a/solx-mlir/tests/lit/delete_function_pointer.sol b/solx-mlir/tests/lit/delete_function_pointer.sol new file mode 100644 index 000000000..d1db6969b --- /dev/null +++ b/solx-mlir/tests/lit/delete_function_pointer.sol @@ -0,0 +1,23 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s + +// solc print-init types the default function constant as the unit signature and stores it into a +// slot of another type; solx types it from the target so the store is consistent. + +// CHECK: sol.func @{{.*reset.*}} +// CHECK: %[[DEFAULT:.*]] = sol.default_func_constant : !sol.func_ref<() -> ui256> +// CHECK: sol.store %[[DEFAULT]], %[[SLOT:.*]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> +// CHECK: %[[G:.*]] = sol.func_constant @{{.*g.*}} : !sol.func_ref<() -> ui256> +// CHECK: %[[CLEARED:.*]] = sol.load %[[SLOT]] : !sol.ptr ui256>, Stack>, !sol.func_ref<() -> ui256> +// CHECK: sol.cmp eq, %[[CLEARED]], %[[G]] : !sol.func_ref<() -> ui256> + +contract C { + function g() internal pure returns (uint256) { + return 1; + } + + function reset() public pure returns (uint256) { + function() internal pure returns (uint256) functionPointer = g; + delete functionPointer; + return functionPointer == g ? 1 : 0; + } +} diff --git a/solx-mlir/tests/lit/evm_context.sol b/solx-mlir/tests/lit/evm_context.sol index 739475a57..f49ccf652 100644 --- a/solx-mlir/tests/lit/evm_context.sol +++ b/solx-mlir/tests/lit/evm_context.sol @@ -38,7 +38,8 @@ // CHECK: sol.prevrandao : ui256 // CHECK: sol.func @{{.*get_balance.*}} -// CHECK: sol.balance %{{.*}} : !sol.address -> ui256 +// CHECK: %[[BALANCE_RECEIVER:.*]] = sol.address_cast %{{.*}} : !sol.address to !sol.address +// CHECK: sol.balance %[[BALANCE_RECEIVER]] : !sol.address -> ui256 contract C { function get_sender() public view returns (address) { @@ -89,7 +90,7 @@ contract C { return block.prevrandao; } - function get_balance(address a) public view returns (uint256) { + function get_balance(address payable a) public view returns (uint256) { return a.balance; } } diff --git a/solx-mlir/tests/lit/internal_function_pointer.sol b/solx-mlir/tests/lit/internal_function_pointer.sol new file mode 100644 index 000000000..c87667c1a --- /dev/null +++ b/solx-mlir/tests/lit/internal_function_pointer.sol @@ -0,0 +1,129 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s + +// CHECK: sol.state_var @{{.*functionPointerState.*}} slot 0 offset 0 : !sol.func_ref<() -> ui256> +// CHECK: sol.state_var @{{.*functionPointerField.*}} slot 1 offset 0 : !sol.struct<(!sol.func_ref<() -> ui256>), Storage> + +// CHECK: sol.func @{{.*g.*}}() -> ui256 attributes {{.*}}id = {{[0-9]+}} + +// CHECK: sol.func @{{.*run.*}} +// CHECK: %[[G:.*]] = sol.func_constant @{{.*g.*}} : !sol.func_ref<() -> ui256> +// CHECK: sol.store %[[G]], %[[SLOT:.*]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> +// CHECK: %[[POINTER:.*]] = sol.load %[[SLOT]] : !sol.ptr ui256>, Stack>, !sol.func_ref<() -> ui256> +// CHECK: sol.icall %[[POINTER]]() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*invoke.*}}(%[[ARGUMENT:.*]]: !sol.func_ref<() -> ui256>) -> ui256 +// CHECK: sol.store %[[ARGUMENT]], %[[SLOT:.*]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> +// CHECK: %[[POINTER:.*]] = sol.load %[[SLOT]] : !sol.ptr ui256>, Stack>, !sol.func_ref<() -> ui256> +// CHECK: sol.icall %[[POINTER]]() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_argument.*}} +// CHECK: sol.func_constant @{{.*g.*}} : !sol.func_ref<() -> ui256> +// CHECK: sol.call @{{.*invoke.*}}(%{{.*}}) : (!sol.func_ref<() -> ui256>) -> ui256 + +// CHECK: sol.func @{{.*set.*}} +// CHECK: sol.store %{{.*}}, %{{.*}} : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Storage> + +// CHECK: sol.func @{{.*run_state.*}} +// CHECK: sol.addr_of @{{.*functionPointerState.*}} : !sol.ptr ui256>, Storage> +// CHECK: sol.load %{{.*}} : !sol.ptr ui256>, Storage>, !sol.func_ref<() -> ui256> +// CHECK: sol.icall %{{[0-9]+}}() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_element.*}} +// CHECK: sol.gep %{{.*}}, %{{.*}} : !sol.array<1 x !sol.func_ref<() -> ui256>, Memory>, {{.*}}!sol.ptr ui256>, Memory> +// CHECK: sol.load %{{.*}} : !sol.ptr ui256>, Memory>, !sol.func_ref<() -> ui256> +// CHECK: sol.icall %{{[0-9]+}}() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_field.*}} +// CHECK: sol.gep %{{.*}}, %{{.*}} : !sol.struct<(!sol.func_ref<() -> ui256>), Storage>, ui64, !sol.ptr ui256>, Storage> +// CHECK: sol.load %{{.*}} : !sol.ptr ui256>, Storage>, !sol.func_ref<() -> ui256> +// CHECK: sol.icall %{{[0-9]+}}() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_ternary.*}} +// CHECK: %[[SLOT:.*]] = sol.alloca : !sol.ptr ui256>, Stack> +// CHECK: sol.if %{{.*}} { +// CHECK: sol.store %{{.*}}, %[[SLOT]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> +// CHECK: } else { +// CHECK: sol.store %{{.*}}, %[[SLOT]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> +// CHECK: } +// CHECK: sol.icall %{{[0-9]+}}() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_call_result.*}} +// CHECK: %[[PICKED:.*]] = sol.call @{{.*pick.*}} : () -> !sol.func_ref<() -> ui256> +// CHECK: sol.icall %[[PICKED]]() : !sol.func_ref<() -> ui256>, () -> ui256 + +// CHECK: sol.func @{{.*run_arguments_results.*}} +// CHECK: %[[PAIR:.*]] = sol.func_constant @{{.*pair.*}} : !sol.func_ref<(ui256, ui256) -> (ui256, ui256)> +// CHECK: sol.store %[[PAIR]], %[[SLOT:.*]] : !sol.func_ref<(ui256, ui256) -> (ui256, ui256)>, !sol.ptr (ui256, ui256)>, Stack> +// CHECK: %[[POINTER:.*]] = sol.load %[[SLOT]] : !sol.ptr (ui256, ui256)>, Stack>, !sol.func_ref<(ui256, ui256) -> (ui256, ui256)> +// CHECK: sol.icall %[[POINTER]](%{{.*}}, %{{.*}}) : !sol.func_ref<(ui256, ui256) -> (ui256, ui256)>, (ui256, ui256) -> (ui256, ui256) + +contract C { + struct S { + function () internal returns (uint256) f; + } + + function () internal returns (uint256) functionPointerState; + + S functionPointerField; + + function g() internal returns (uint256) { + return 42; + } + + function run() public returns (uint256) { + function () internal returns (uint256) functionPointer = g; + return functionPointer(); + } + + function invoke(function () internal returns (uint256) f) internal returns (uint256) { + return f(); + } + + function run_argument() public returns (uint256) { + return invoke(g); + } + + function set() public { + functionPointerState = g; + } + + function run_state() public returns (uint256) { + return functionPointerState(); + } + + function run_element() public returns (uint256) { + function () internal returns (uint256)[1] memory functionPointers = [g]; + return functionPointers[0](); + } + + function run_field() public returns (uint256) { + return functionPointerField.f(); + } + + function k() internal returns (uint256) { + return 7; + } + + function run_ternary(bool condition) public returns (uint256) { + function () internal returns (uint256) functionPointer = condition ? g : k; + return functionPointer(); + } + + function pick() internal returns (function () internal returns (uint256)) { + return g; + } + + function run_call_result() public returns (uint256) { + return pick()(); + } + + function pair(uint256 a, uint256 b) internal returns (uint256, uint256) { + return (a, b); + } + + function run_arguments_results() public returns (uint256, uint256) { + function (uint256, uint256) internal returns (uint256, uint256) functionPointer = pair; + return functionPointer(7, 9); + } +} diff --git a/solx-mlir/tests/lit/value_transfer.sol b/solx-mlir/tests/lit/value_transfer.sol index 3ebb1b431..ac18c8391 100644 --- a/solx-mlir/tests/lit/value_transfer.sol +++ b/solx-mlir/tests/lit/value_transfer.sol @@ -1,12 +1,13 @@ // RUN: solx --emit-mlir=sol %s | FileCheck %s - -// solx-only: solc down-casts the payable operand to a plain `address` before send/transfer (an -// extra `sol.address_cast`), so it diverges here; solx keeps the operand payable. +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s // CHECK: sol.func {{.*}}pay_send{{.*}}-> i1 -// CHECK: sol.send {{.*}}, {{.*}} : !sol.address, ui256 -> i1 +// CHECK: %[[SR:.*]] = sol.address_cast %{{.*}} : !sol.address to !sol.address +// CHECK: sol.send %[[SR]], %{{.*}} : !sol.address, ui256 -> i1 + // CHECK: sol.func {{.*}}pay_transfer{{.*}}!sol.address{{.*}}ui256 -// CHECK: sol.transfer {{.*}}, {{.*}} : !sol.address, ui256 +// CHECK: %[[TR:.*]] = sol.address_cast %{{.*}} : !sol.address to !sol.address +// CHECK: sol.transfer %[[TR]], %{{.*}} : !sol.address, ui256 contract C { function pay_send(address payable r, uint256 v) public returns (bool) { return r.send(v); } diff --git a/solx-slang/Cargo.toml b/solx-slang/Cargo.toml index 3c996ab87..88e510974 100644 --- a/solx-slang/Cargo.toml +++ b/solx-slang/Cargo.toml @@ -17,10 +17,11 @@ num-traits = "0.2" ruint.workspace = true serde_json.workspace = true semver.workspace = true + slang_solidity_v2.workspace = true solx-codegen-evm = { path = "../solx-codegen-evm" } solx-core = { path = "../solx-core", features = ["mlir"] } solx-mlir = { path = "../solx-mlir" } solx-standard-json = { path = "../solx-standard-json", features = ["mlir"] } -solx-utils = { path = "../solx-utils", features = ["slang"] } +solx-utils = { path = "../solx-utils" } diff --git a/solx-slang/src/contract/function/expression/call/mod.rs b/solx-slang/src/contract/function/expression/call/mod.rs index fbc14a7ff..4d26ebf33 100644 --- a/solx-slang/src/contract/function/expression/call/mod.rs +++ b/solx-slang/src/contract/function/expression/call/mod.rs @@ -24,8 +24,8 @@ use solx_mlir::Value; use crate::scope::function::FunctionScope; /// The one emission kind a function call's callee resolves to, owning both the classification and -/// the emission of each kind. The variants are mutually exclusive and tested in declaration order, -/// so an earlier match wins. +/// the emission of each kind. The variants are mutually exclusive: `from_call` resolves a callee to +/// exactly one. pub enum Call { /// The callee names a struct, so the call builds a struct value from its members. StructConstruction(StructDefinition), @@ -39,6 +39,8 @@ pub enum Call { Member(MemberAccessExpression), /// A direct call to a named function. Function(FunctionDefinition), + /// A call through a function-typed value, dispatched by `sol.icall`. + FunctionPointer, } impl Call { @@ -68,6 +70,7 @@ impl Call { Self::Function(function_definition) => { scope.call(&function_definition, arguments.iter()) } + Self::FunctionPointer => Self::function_pointer(node, arguments, scope), } } @@ -93,17 +96,27 @@ impl Call { if let Some(built_in) = identifier.resolve_to_built_in() { return Self::Builtin(built_in); } - let Some(Definition::Function(function_definition)) = + if let Some(Definition::Function(function_definition)) = identifier.resolve_to_definition() - else { - unimplemented!( - "callee '{}' does not resolve to a function", - identifier.name() - ); - }; - Self::Function(function_definition) + { + return Self::Function(function_definition); + } + if matches!(identifier.get_type(), Some(Type::Function(_))) { + return Self::FunctionPointer; + } + unimplemented!("unsupported callee '{}'", identifier.name()) } - Expression::MemberAccessExpression(access) => Self::Member(access), + Expression::MemberAccessExpression(access) => { + if matches!( + access.member().resolve_to_definition(), + Some(Definition::StructMember(_)) + ) && matches!(access.get_type(), Some(Type::Function(_))) + { + return Self::FunctionPointer; + } + Self::Member(access) + } + callee if matches!(callee.get_type(), Some(Type::Function(_))) => Self::FunctionPointer, callee => unimplemented!( "unsupported callee expression: {:?}", std::mem::discriminant(&callee) @@ -276,12 +289,14 @@ impl Call { ) -> Option> { match access.member().resolve_to_built_in() { Some(BuiltIn::AddressSend) => { - let address = scope.expression(&access.operand()); + let address = + scope.converted(&access.operand(), MlirType::address(scope.melior, false)); let values = scope.positional_arguments(arguments); Some(Value::send(address, values[0], scope)) } Some(BuiltIn::AddressTransfer) => { - let address = scope.expression(&access.operand()); + let address = + scope.converted(&access.operand(), MlirType::address(scope.melior, false)); let values = scope.positional_arguments(arguments); Value::transfer(address, values[0], scope); None @@ -408,6 +423,27 @@ impl Call { _ => unimplemented!("unsupported member call: {}", access.member().name()), } } + + /// The signature comes from the callee's binder type: a pointer callee names no definition to + /// look a registered signature up by. + fn function_pointer<'context>( + call: &FunctionCallExpression, + arguments: &PositionalArguments, + scope: &mut FunctionScope<'_, '_, 'context>, + ) -> Vec> { + let callee = call.operand(); + let Some(Type::Function(function_type)) = callee.get_type() else { + unreachable!("classification admits only function-typed callees"); + }; + let function_type = scope.contract.source_unit.function_type(&function_type); + let pointer = scope.expression(&callee); + let converted: Vec> = arguments + .iter() + .zip(&function_type.parameters) + .map(|(argument, ¶meter_type)| scope.converted(&argument, parameter_type)) + .collect(); + pointer.indirect_call(&converted, &function_type.results, scope) + } } impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { @@ -450,15 +486,14 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' .function_signature(function_definition.node_id()); let converted: Vec> = arguments .into_iter() - .zip(&signature.parameter_types) + .zip(&signature.function_type.parameters) .map(|(argument, ¶meter_type)| self.converted(&argument, parameter_type)) .collect(); Function::call( &signature.mlir_name, &converted, - &signature.return_types, + &signature.function_type.results, self, ) - .expect("sol.call yields its declared results") } } diff --git a/solx-slang/src/contract/function/expression/identifier.rs b/solx-slang/src/contract/function/expression/identifier.rs index fa24ed7bb..e104a10ef 100644 --- a/solx-slang/src/contract/function/expression/identifier.rs +++ b/solx-slang/src/contract/function/expression/identifier.rs @@ -1,5 +1,6 @@ //! -//! Identifier references: constants fold, variables load from their places. +//! Identifier references: constants fold, function names materialise pointers, variables load from +//! their places. //! use slang_solidity_v2::ast::Definition; @@ -12,13 +13,23 @@ use solx_mlir::Value; use crate::scope::function::FunctionScope; impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, 'context> { - /// A constant folds to its initializer; every other identifier loads from its place. + /// A constant folds to its initializer; a bare function name materialises its internal pointer + /// (`sol.func_constant`); every other identifier loads from its place. pub fn identifier(&mut self, node: &Identifier) -> Value<'context> { - if let Some(Definition::Constant(constant)) = node.resolve_to_definition() { - return self.expression(&constant.value().expect("constant has an initializer")); + match node.resolve_to_definition() { + Some(Definition::Constant(constant)) => { + self.expression(&constant.value().expect("constant has an initializer")) + } + Some(Definition::Function(function)) => self + .contract + .source_unit + .function_signature(function.node_id()) + .pointer_constant(self), + _ => { + let (place, element_type) = self.identifier_place(node); + place.load(element_type, self) + } } - let (place, element_type) = self.identifier_place(node); - place.load(element_type, self) } /// A state variable resolves to its storage slot, a local variable or parameter to its stack diff --git a/solx-slang/src/contract/function/expression/member.rs b/solx-slang/src/contract/function/expression/member.rs index 1f1b7f9b3..57fcfc81c 100644 --- a/solx-slang/src/contract/function/expression/member.rs +++ b/solx-slang/src/contract/function/expression/member.rs @@ -35,11 +35,21 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' return Value::constant_from_bigint(&BigInt::from(ordinal), enum_type, self); } match node.member().resolve_to_built_in() { - Some(BuiltIn::AddressBalance) => Value::balance(self.expression(&node.operand()), self), + Some(BuiltIn::AddressBalance) => { + let address = + self.converted(&node.operand(), MlirType::address(self.melior, false)); + Value::balance(address, self) + } Some(BuiltIn::AddressCodehash) => { - Value::code_hash(self.expression(&node.operand()), self) + let address = + self.converted(&node.operand(), MlirType::address(self.melior, false)); + Value::code_hash(address, self) + } + Some(BuiltIn::AddressCode) => { + let address = + self.converted(&node.operand(), MlirType::address(self.melior, false)); + Value::code(address, self) } - Some(BuiltIn::AddressCode) => Value::code(self.expression(&node.operand()), self), Some(BuiltIn::Length) => self.expression(&node.operand()).length(self), Some(BuiltIn::TxOrigin) => Value::tx_origin(self), Some(BuiltIn::TxGasPrice) => Value::tx_gas_price(self), @@ -65,8 +75,7 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' } } - /// The address yielded by `s.field` together with the field's element MLIR type. The field index - /// is derived by member-name comparison until node-id resolution is verified against the corpus. + /// The address yielded by `s.field` together with the field's element MLIR type. pub fn member_access_place( &mut self, node: &MemberAccessExpression, @@ -79,13 +88,14 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' unreachable!("slang StructType always references a Struct definition"); }; - let member = node.member(); - let member_name = member.name(); + let Some(Definition::StructMember(member)) = node.member().resolve_to_definition() else { + unreachable!("a struct field access binds to the member it names"); + }; let field_index = struct_definition .members() .iter() - .position(|member| member.name().name() == member_name) - .expect("slang validates the accessed member exists"); + .position(|candidate| candidate.node_id() == member.node_id()) + .expect("a struct lists the members it declares"); let base_value = self.expression(&base); let element_type = base_value.r#type().element_type(field_index as u64); diff --git a/solx-slang/src/contract/function/mod.rs b/solx-slang/src/contract/function/mod.rs index 44236a144..61190fe49 100644 --- a/solx-slang/src/contract/function/mod.rs +++ b/solx-slang/src/contract/function/mod.rs @@ -8,9 +8,11 @@ pub mod statement; use slang_solidity_v2::ast::ContractDefinition; use slang_solidity_v2::ast::FunctionDefinition; use slang_solidity_v2::ast::FunctionKind; -use slang_solidity_v2::ast::FunctionMutability; use solx_mlir::Function; +use solx_mlir::FunctionDispatch; +use solx_mlir::FunctionKind as MlirFunctionKind; +use solx_mlir::FunctionType; use solx_mlir::Place; use solx_mlir::StateMutability; use solx_mlir::Value; @@ -26,37 +28,24 @@ impl<'source_unit, 'context> ContractScope<'source_unit, 'context> { return; }; let signature = self.source_unit.function_signature(function.node_id()); - let state_mutability = match function.attributes().mutability() { - FunctionMutability::Pure => StateMutability::Pure, - FunctionMutability::View => StateMutability::View, - FunctionMutability::Payable => StateMutability::Payable, - FunctionMutability::NonPayable => StateMutability::NonPayable, - }; - let mlir_kind = match function.kind() { - FunctionKind::Constructor => Some(solx_mlir::FunctionKind::Constructor), - FunctionKind::Fallback => Some(solx_mlir::FunctionKind::Fallback), - FunctionKind::Receive => Some(solx_mlir::FunctionKind::Receive), - FunctionKind::Regular => None, - FunctionKind::Modifier => unreachable!("modifiers are filtered before emission"), - }; + let state_mutability = StateMutability::from(function.attributes().mutability()); let entry = signature.define( function.compute_selector(), + FunctionDispatch::from(function), state_mutability, - mlir_kind, self, self.contract_body, ); - let Function { - parameter_types, - return_types, - .. - } = signature; - self.function(entry, return_types, |scope| { + let FunctionType { + parameters, + results, + } = signature.function_type; + self.function(entry, results, |scope| { for (index, parameter) in function.parameters().iter().enumerate() { let Some(identifier) = parameter.name() else { continue; }; - scope.define_local(identifier.name(), parameter_types[index], |_scope| { + scope.define_local(identifier.name(), parameters[index], |_scope| { entry.argument(index) }); } @@ -112,8 +101,8 @@ impl<'source_unit, 'context> ContractScope<'source_unit, 'context> { } let entry = Function::constructor().define( None, + FunctionDispatch::Kind(MlirFunctionKind::Constructor), StateMutability::NonPayable, - Some(solx_mlir::FunctionKind::Constructor), self, self.contract_body, ); diff --git a/solx-slang/src/contract/mod.rs b/solx-slang/src/contract/mod.rs index 9fba37612..d340d2ac2 100644 --- a/solx-slang/src/contract/mod.rs +++ b/solx-slang/src/contract/mod.rs @@ -13,6 +13,7 @@ use slang_solidity_v2::ast::ContractDefinition; use slang_solidity_v2::ast::ContractMember; use slang_solidity_v2::ast::FunctionDefinition; use slang_solidity_v2::ast::FunctionKind; +use slang_solidity_v2::ast::Type; use solx_mlir::Block; use solx_mlir::Contract; @@ -41,28 +42,16 @@ impl<'context> SourceUnitScope<'context> { .chain(node.constructor()) .chain(operator_functions.iter().cloned()) { - let parameter_types = function - .parameters() - .iter() - .map(|parameter| self.typing(parameter.get_type())) - .collect(); - let return_types = function - .returns() - .map(|returns| { - returns - .iter() - .map(|parameter| self.typing(parameter.get_type())) - .collect() - }) - .unwrap_or_default(); + let Some(Type::Function(function_type)) = function.get_type() else { + unreachable!("slang types every function definition"); + }; self.function_signatures.insert( function.node_id(), Function::new( function .compute_internal_signature() .expect("every emitted function has an internal signature"), - parameter_types, - return_types, + self.function_type(&function_type), ), ); } diff --git a/solx-slang/src/source_unit.rs b/solx-slang/src/source_unit.rs index 86e39f3d2..629109358 100644 --- a/solx-slang/src/source_unit.rs +++ b/solx-slang/src/source_unit.rs @@ -42,10 +42,10 @@ impl<'context> SourceUnitScope<'context> { let mut contracts = BTreeMap::new(); for contract in unit.contracts().iter().filter(|contract| { !contract.is_abstract() - && !contract - .direct_bases() - .iter() - .any(|base| matches!(base, ContractBase::Contract(_))) + && !contract.direct_bases().iter().any(|base| match base { + ContractBase::Contract(_) => true, + ContractBase::Interface(_) => false, + }) }) { let mut scope = SourceUnitScope::new(Context::new(&melior, evm_version)); let method_identifiers = scope.contract_definition(contract, &operator_functions); diff --git a/solx-slang/src/type.rs b/solx-slang/src/type.rs index d5e2a7f0a..e0d4bc692 100644 --- a/solx-slang/src/type.rs +++ b/solx-slang/src/type.rs @@ -4,10 +4,13 @@ use num_traits::sign::Signed; use slang_solidity_v2::ast::Definition; +use slang_solidity_v2::ast::FunctionType as SlangFunctionType; +use slang_solidity_v2::ast::FunctionTypeVisibility; use slang_solidity_v2::ast::LiteralKind; use slang_solidity_v2::ast::Type; use solx_mlir::ArraySize; +use solx_mlir::FunctionType; use solx_mlir::Type as MlirType; use crate::scope::source_unit::SourceUnitScope; @@ -160,6 +163,14 @@ impl<'context> SourceUnitScope<'context> { let max = u8::try_from(member_count - 1).expect("enum member count fits in u8"); MlirType::enumeration(self.melior, max.into()) } + Type::Function(function_type) => { + if matches!(function_type.visibility(), FunctionTypeVisibility::External) { + unimplemented!( + "MLIR type resolution is not yet implemented for external function types" + ); + } + self.function_type(function_type).reference(self.melior) + } Type::UserDefinedValue(udvt) => { let target_type = udvt .target_type() @@ -170,6 +181,27 @@ impl<'context> SourceUnitScope<'context> { } } + /// Resolves a function type's MLIR signature. Any non-external visibility resolves here: + /// `Public` types a bare reference to a public function, a legal internal pointer. + pub fn function_type(&self, function_type: &SlangFunctionType) -> FunctionType<'context> { + FunctionType { + parameters: function_type + .parameter_types() + .iter() + .map(|parameter_type| self.resolve(parameter_type, None)) + .collect(), + results: match function_type.return_type() { + Type::Void(_) => Vec::new(), + Type::Tuple(tuple_type) => tuple_type + .types() + .iter() + .map(|element_type| self.resolve(element_type, None)) + .collect(), + other => vec![self.resolve(&other, None)], + }, + } + } + /// Resolves the binder's typing of a node to its Sol dialect MLIR type. pub fn typing(&self, slang_type: Option) -> MlirType<'context> { self.resolve( diff --git a/solx-utils/Cargo.toml b/solx-utils/Cargo.toml index 25b8c7cd8..dee08503e 100644 --- a/solx-utils/Cargo.toml +++ b/solx-utils/Cargo.toml @@ -22,8 +22,4 @@ serde_arrays = "0.2" ipfs-hasher = "0.13" base58 = "0.2" -slang_solidity_v2 = { workspace = true, optional = true } - -[features] -mlir = [] -slang = ["dep:slang_solidity_v2"] +slang_solidity_v2.workspace = true diff --git a/solx-utils/src/data_location.rs b/solx-utils/src/data_location.rs index 968d89234..55be7bd48 100644 --- a/solx-utils/src/data_location.rs +++ b/solx-utils/src/data_location.rs @@ -53,7 +53,6 @@ impl From for DataLocation { } } -#[cfg(feature = "slang")] impl DataLocation { /// Converts a Slang semantic data location into the dialect's data location. /// diff --git a/solx-utils/src/evm_version.rs b/solx-utils/src/evm_version.rs index 068d57dcb..46a06d824 100644 --- a/solx-utils/src/evm_version.rs +++ b/solx-utils/src/evm_version.rs @@ -54,7 +54,6 @@ impl FromStr for EVMVersion { } } -#[cfg(feature = "mlir")] impl EVMVersion { /// Returns the Sol dialect `EvmVersionAttr` integer encoding. pub fn into_sol_dialect_identifier(self) -> u32 { diff --git a/solx-utils/src/extension.rs b/solx-utils/src/extension.rs index da1d730bf..eb4da4412 100644 --- a/solx-utils/src/extension.rs +++ b/solx-utils/src/extension.rs @@ -39,7 +39,6 @@ pub static EXTENSION_LLVM_SOURCE: &str = "ll"; pub static EXTENSION_LLVM_BINARY: &str = "bc"; /// The MLIR source code file extension. -#[cfg(feature = "mlir")] pub static EXTENSION_MLIR: &str = "mlir"; /// The Solidity AST file extension. From 6fd38e4d1fd4c4907fbab7aada108946ac22baa1 Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Sat, 1 Aug 2026 17:56:42 +0400 Subject: [PATCH 08/11] fix(slang): qualify function symbols with the node id (#621) --- .../evaluation_order/nested_assignment.sol | 40 +++++----- .../lit/evaluation_order/nested_binary.sol | 28 +++---- .../lit/evaluation_order/nested_call.sol | 18 ++--- .../lit/evaluation_order/nested_index.sol | 20 ++--- .../lit/evaluation_order/nested_logical.sol | 22 +++--- .../lit/evaluation_order/nested_ternary.sol | 30 ++++---- solx-mlir/tests/lit/function_overloads.sol | 77 +++++++++++++++++++ solx-mlir/tests/lit/function_shadowing.sol | 43 +++++++++++ solx-slang/src/contract/function/mod.rs | 15 ++++ solx-slang/src/contract/mod.rs | 7 +- 10 files changed, 215 insertions(+), 85 deletions(-) create mode 100644 solx-mlir/tests/lit/function_overloads.sol create mode 100644 solx-mlir/tests/lit/function_shadowing.sol diff --git a/solx-mlir/tests/lit/evaluation_order/nested_assignment.sol b/solx-mlir/tests/lit/evaluation_order/nested_assignment.sol index 13d29bbc9..002db0f2e 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_assignment.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_assignment.sol @@ -4,53 +4,53 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*ternary.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*call.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*indexPlace.*}} // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*compound.*}} // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*tuple.*}} // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/evaluation_order/nested_binary.sol b/solx-mlir/tests/lit/evaluation_order/nested_binary.sol index cfd0e34a8..38cb17b06 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_binary.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_binary.sol @@ -4,39 +4,39 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*ternary.*}} // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*assignment.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*call.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*index.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/evaluation_order/nested_call.sol b/solx-mlir/tests/lit/evaluation_order/nested_call.sol index eaeb7fcd6..47af3c18b 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_call.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_call.sol @@ -4,25 +4,25 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*ternary.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*assignment.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/evaluation_order/nested_index.sol b/solx-mlir/tests/lit/evaluation_order/nested_index.sol index ecefa12b2..297bf2c3a 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_index.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_index.sol @@ -4,27 +4,27 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*ternary.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*call.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/evaluation_order/nested_logical.sol b/solx-mlir/tests/lit/evaluation_order/nested_logical.sol index 7bd8fb8ba..0c5b451a4 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_logical.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_logical.sol @@ -4,36 +4,36 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*ternary.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*assignment.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*logical.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/evaluation_order/nested_ternary.sol b/solx-mlir/tests/lit/evaluation_order/nested_ternary.sol index 697173326..5860bea94 100644 --- a/solx-mlir/tests/lit/evaluation_order/nested_ternary.sol +++ b/solx-mlir/tests/lit/evaluation_order/nested_ternary.sol @@ -4,43 +4,43 @@ // CHECK: sol.func @{{.*binary.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*logical.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*call.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 4 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.func @{{.*assignment.*}} // CHECK: sol.constant 1 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.if // CHECK: sol.constant 2 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" // CHECK: sol.constant 3 : ui8 -// CHECK: sol.call @"t(uint256)" +// CHECK: sol.call @"t(uint256)_{{[0-9]+}}" diff --git a/solx-mlir/tests/lit/function_overloads.sol b/solx-mlir/tests/lit/function_overloads.sol new file mode 100644 index 000000000..a00760cd5 --- /dev/null +++ b/solx-mlir/tests/lit/function_overloads.sol @@ -0,0 +1,77 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s + +// CHECK: sol.func @{{.*pick.*}}(%{{.*}}: ui256) -> ui256 +// CHECK: sol.func @{{.*pick.*}}(%{{.*}}: i1) -> i1 + +// CHECK: sol.func @{{.*sum.*}}(%{{.*}}: ui256) -> ui256 +// CHECK: sol.func @{{.*sum.*}}(%{{.*}}: ui256, %{{.*}}: ui256) -> ui256 + +// CHECK: sol.func @{{.*run_nested.*}} +// CHECK: %[[INNER:.*]] = sol.call @{{.*sum.*}}(%{{.*}}) : (ui256) -> ui256 +// CHECK: sol.call @{{.*sum.*}}(%[[INNER]], %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*get.*}}(%{{.*}}: ui256) -> ui256 attributes {{.*}}selector = -1794649190 +// CHECK: sol.func @{{.*get.*}}(%{{.*}}: i1) -> i1 attributes {{.*}}selector = -1044471942 + +// CHECK: sol.func @{{.*apply_pointer.*}}(%{{.*}}: !sol.func_ref<(ui256) -> ui256>) -> ui256 +// CHECK: sol.icall %{{[0-9]+}}(%{{.*}}) : !sol.func_ref<(ui256) -> ui256>, (ui256) -> ui256 +// CHECK: sol.func @{{.*apply_pointer.*}}(%{{.*}}: !sol.func_ref<(i1) -> i1>) -> i1 +// CHECK: sol.icall %{{[0-9]+}}(%{{.*}}) : !sol.func_ref<(i1) -> i1>, (i1) -> i1 + +// CHECK: sol.func @{{.*run.*}} +// CHECK: sol.call @{{.*apply_pointer.*}}(%{{.*}}) : (!sol.func_ref<(i1) -> i1>) -> i1 +// CHECK: sol.call @{{.*pick.*}}(%{{.*}}) : (i1) -> i1 + +contract C { + function pick(uint256 x) internal pure returns (uint256) { + return x; + } + + function pick(bool b) internal pure returns (bool) { + return b; + } + + function sum(uint256 x) internal pure returns (uint256) { + return x; + } + + function sum(uint256 x, uint256 y) internal pure returns (uint256) { + return x + y; + } + + function run_nested() public pure returns (uint256) { + return sum(sum(10), 20); + } + + function get(uint256 x) public pure returns (uint256) { + return x; + } + + function get(bool b) public pure returns (bool) { + return b; + } + + function apply_pointer(function(uint256) internal pure returns (uint256) f) internal pure returns (uint256) { + return f(1); + } + + function apply_pointer(function(bool) internal pure returns (bool) f) internal pure returns (bool) { + return f(true); + } + + function first(uint256 x) internal pure returns (uint256) { + return x + 1; + } + + function second(bool b) internal pure returns (bool) { + return !b; + } + + function run() public pure returns (uint256) { + if (apply_pointer(second) || pick(true)) { + return apply_pointer(first) + pick(1); + } + return 0; + } +} diff --git a/solx-mlir/tests/lit/function_shadowing.sol b/solx-mlir/tests/lit/function_shadowing.sol new file mode 100644 index 000000000..a0b13d0f4 --- /dev/null +++ b/solx-mlir/tests/lit/function_shadowing.sol @@ -0,0 +1,43 @@ +// RUN: solx --emit-mlir=sol %s | FileCheck %s +// RUN: solc --mlir-action=print-init %s 2>/dev/null | FileCheck %s + +// CHECK: sol.func @{{.*add.*}}(%{{.*}}: ui256, %{{.*}}: ui256) -> ui256 + +// CHECK: sol.func @{{.*plus.*}}(%{{.*}}: ui256, %{{.*}}: ui256) -> ui256 +// CHECK: sol.call @{{.*add.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*sum.*}}(%{{.*}}: ui256, %{{.*}}: ui256) -> ui256 +// CHECK: sol.call @{{.*add.*}}(%{{.*}}, %{{.*}}) : (ui256, ui256) -> ui256 + +// CHECK: sol.func @{{.*f.*}}() -> ui256 +// CHECK: sol.call @{{.*f.*}}() : () -> ui256 + +type T is uint256; + +using {add as +} for T global; + +function add(T a, T b) pure returns (T) { + return T.wrap(T.unwrap(a) + T.unwrap(b)); +} + +function f() pure returns (uint256) { + return 1337; +} + +contract C { + function add(T a, T b) internal pure returns (T) { + return T.wrap(T.unwrap(a) + T.unwrap(b) + 1); + } + + function plus(T a, T b) public pure returns (T) { + return a + b; + } + + function sum(T a, T b) public pure returns (T) { + return add(a, b); + } + + function f() public pure returns (uint256) { + return f(); + } +} diff --git a/solx-slang/src/contract/function/mod.rs b/solx-slang/src/contract/function/mod.rs index 61190fe49..0327ba675 100644 --- a/solx-slang/src/contract/function/mod.rs +++ b/solx-slang/src/contract/function/mod.rs @@ -18,6 +18,7 @@ use solx_mlir::StateMutability; use solx_mlir::Value; use crate::scope::contract::ContractScope; +use crate::scope::source_unit::SourceUnitScope; impl<'source_unit, 'context> ContractScope<'source_unit, 'context> { /// Emits `function`'s `sol.func` into the contract body from its pre-registered signature, @@ -112,3 +113,17 @@ impl<'source_unit, 'context> ContractScope<'source_unit, 'context> { }); } } + +impl<'context> SourceUnitScope<'context> { + /// The function's symbol: its internal signature qualified by the node id, since internal + /// signatures alone collide. + pub fn symbol(function: &FunctionDefinition) -> String { + format!( + "{}_{}", + function + .compute_internal_signature() + .expect("every emitted function has an internal signature"), + function.node_id(), + ) + } +} diff --git a/solx-slang/src/contract/mod.rs b/solx-slang/src/contract/mod.rs index d340d2ac2..01635d87b 100644 --- a/solx-slang/src/contract/mod.rs +++ b/solx-slang/src/contract/mod.rs @@ -47,12 +47,7 @@ impl<'context> SourceUnitScope<'context> { }; self.function_signatures.insert( function.node_id(), - Function::new( - function - .compute_internal_signature() - .expect("every emitted function has an internal signature"), - self.function_type(&function_type), - ), + Function::new(Self::symbol(&function), self.function_type(&function_type)), ); } From e6f9d899f9f8024dc5c3d0aaf7e4fdb92450b212 Mon Sep 17 00:00:00 2001 From: Oleksandr Zarudnyi Date: Tue, 4 Aug 2026 13:05:44 +0400 Subject: [PATCH 09/11] feat(slang): named arguments (#625) --- solx-mlir/build.rs | 2 +- solx-mlir/src/context/contract.rs | 5 +- solx-mlir/src/ir/mod.rs | 20 +- solx-mlir/src/ir/value.rs | 2 +- solx-mlir/src/macros.rs | 81 ++++---- solx-mlir/tests/lit/array_string_ops.sol | 15 ++ .../lit/evaluation_order/named_arguments.sol | 25 +++ solx-mlir/tests/lit/events.sol | 10 + solx-mlir/tests/lit/function_calls.sol | 25 +++ solx-mlir/tests/lit/function_overloads.sol | 13 ++ .../tests/lit/internal_function_pointer.sol | 9 + solx-mlir/tests/lit/revert.sol | 15 ++ solx-mlir/tests/lit/struct_constructor.sol | 17 ++ .../function/expression/arithmetic.rs | 5 +- .../function/expression/call/arguments.rs | 48 +++-- .../contract/function/expression/call/mod.rs | 173 +++++++++++------- .../statement/variable_declaration.rs | 3 +- solx-slang/src/contract/storage_slot.rs | 5 +- solx-slang/src/scope/function.rs | 2 +- solx-slang/src/source_unit.rs | 10 +- solx/tests/cli/stack_too_deep.rs | 1 + .../simple/constant_expressions/bitwise.sol | 2 +- .../evaluation_order/named_arguments.sol | 44 +++++ 23 files changed, 375 insertions(+), 157 deletions(-) create mode 100644 solx-mlir/tests/lit/evaluation_order/named_arguments.sol create mode 100644 tests/solidity/simple/evaluation_order/named_arguments.sol diff --git a/solx-mlir/build.rs b/solx-mlir/build.rs index 66a384b53..904a9f3ec 100644 --- a/solx-mlir/build.rs +++ b/solx-mlir/build.rs @@ -64,7 +64,7 @@ fn main() { cc::Build::new() .cpp(true) .file("sol_attr_stubs.cpp") - .include(&include_path) + .flag(format!("-isystem{}", include_path.display())) .flag("-std=c++17") .compile("sol_attr_stubs"); } diff --git a/solx-mlir/src/context/contract.rs b/solx-mlir/src/context/contract.rs index fff69f192..6512b9db5 100644 --- a/solx-mlir/src/context/contract.rs +++ b/solx-mlir/src/context/contract.rs @@ -49,7 +49,7 @@ impl<'context> Contract<'context> { byte_offset: u32, context: &Context<'context>, ) { - mlir_op_void!( + mlir_op!( context, self.body, StateVarOperation @@ -62,7 +62,8 @@ impl<'context> Contract<'context> { .byte_offset(IntegerAttribute::new( IntegerType::new(context.melior, solx_utils::BIT_LENGTH_X32 as u32).into(), byte_offset.into(), - )) + )); + () ); } } diff --git a/solx-mlir/src/ir/mod.rs b/solx-mlir/src/ir/mod.rs index 6e5255b45..fb43168b7 100644 --- a/solx-mlir/src/ir/mod.rs +++ b/solx-mlir/src/ir/mod.rs @@ -70,23 +70,23 @@ sol_ops! { CmpOperation.predicate(predicate_attr(predicate)).lhs(self).rhs(other).result(boolean()) } - Value::add(self, rhs: value) -> value { - checked(CAddOperation, AddOperation).lhs(self).rhs(rhs) + Value::add(self, rhs: value) -> value checked(CAddOperation) { + AddOperation.lhs(self).rhs(rhs) } - Value::subtract(self, rhs: value) -> value { - checked(CSubOperation, SubOperation).lhs(self).rhs(rhs) + Value::subtract(self, rhs: value) -> value checked(CSubOperation) { + SubOperation.lhs(self).rhs(rhs) } - Value::multiply(self, rhs: value) -> value { - checked(CMulOperation, MulOperation).lhs(self).rhs(rhs) + Value::multiply(self, rhs: value) -> value checked(CMulOperation) { + MulOperation.lhs(self).rhs(rhs) } - Value::divide(self, rhs: value) -> value { - checked(CDivOperation, DivOperation).lhs(self).rhs(rhs) + Value::divide(self, rhs: value) -> value checked(CDivOperation) { + DivOperation.lhs(self).rhs(rhs) } Value::remainder(self, rhs: value) -> value { ModOperation.lhs(self).rhs(rhs) } - Value::exponentiate(self, rhs: value) -> value { - checked(CExpOperation, ExpOperation).lhs(self).rhs(rhs).result(self_ty) + Value::exponentiate(self, rhs: value) -> value checked(CExpOperation) { + ExpOperation.lhs(self).rhs(rhs).result(self_ty) } Value::bitand(self, rhs: value) -> value { AndOperation.lhs(self).rhs(rhs) diff --git a/solx-mlir/src/ir/value.rs b/solx-mlir/src/ir/value.rs index 7878d579b..900ef5d00 100644 --- a/solx-mlir/src/ir/value.rs +++ b/solx-mlir/src/ir/value.rs @@ -69,7 +69,7 @@ impl<'context> Value<'context> { Self::constant(i64::from(value), Type::boolean(context.melior), context) } - /// Materialises the default-initialized value of `target_type`, matching solc's default-init. + /// Materialises the default-initialized value of `target_type`. /// A function reference cannot route through `zero`, since `sol.constant` is illegal at a /// `!sol.func_ref`, and a reference type's default is its location's designator rather than a /// value. diff --git a/solx-mlir/src/macros.rs b/solx-mlir/src/macros.rs index f8db7d5ff..3cc9ca682 100644 --- a/solx-mlir/src/macros.rs +++ b/solx-mlir/src/macros.rs @@ -1,9 +1,9 @@ //! //! ODS op-construction macros. //! -//! `mlir_op_build!` / `mlir_op!` / `mlir_op_void!` / `mlir_region_op!` collapse the ceremony of an -//! ODS-generated op builder (the `(context, unknown_location)` head and `.build().into()` tail) so a -//! site states only the op name and its setters. +//! `mlir_op_build!` / `mlir_op!` / `mlir_region_op!` collapse the ceremony of an ODS-generated op +//! builder (the `(context, unknown_location)` head and `.build().into()` tail) so a site states only +//! the op name and its setters. //! /// Builds an inlined dialect op and yields it as an `Operation`, without appending. @@ -18,11 +18,15 @@ macro_rules! mlir_op_build { /// Builds an inlined dialect op ([`mlir_op_build!`]), appends it to `$block`, and /// returns its single result value. The `expect` message is derived from the op. -/// Omitting `$block` appends at the `current_block()` cursor. +/// Omitting `$block` appends at the `current_block()` cursor. A `; ()` tail marks the value-less +/// form of a `$block` site: the op is appended for its effect and yields `()`. macro_rules! mlir_op { ($context:expr, $operation:ident $(.$method:ident($($argument:expr),* $(,)?))*) => { mlir_op!($context, $context.current_block(), $operation $(.$method($($argument),*))*) }; + ($context:expr, $block:expr, $operation:ident $(.$method:ident($($argument:expr),* $(,)?))* ; ()) => { + $block.append_operation(mlir_op_build!($context, $operation $(.$method($($argument),*))*)); + }; ($context:expr, $block:expr, $operation:ident $(.$method:ident($($argument:expr),* $(,)?))*) => { $block .append_operation(mlir_op_build!($context, $operation $(.$method($($argument),*))*)) @@ -31,17 +35,6 @@ macro_rules! mlir_op { }; } -/// [`mlir_op!`] for a value-less op: a statement or effect such as `sol.store` -/// or `sol.return`: appends the op ([`mlir_op_build!`]) and yields `()`. -macro_rules! mlir_op_void { - ($context:expr, $operation:ident $(.$method:ident($($argument:expr),* $(,)?))*) => { - mlir_op_void!($context, $context.current_block(), $operation $(.$method($($argument),*))*) - }; - ($context:expr, $block:expr, $operation:ident $(.$method:ident($($argument:expr),* $(,)?))*) => { - $block.append_operation(mlir_op_build!($context, $operation $(.$method($($argument),*))*)); - }; -} - /// Appends a region-bearing control-flow op (`sol.if`/`for`/`while`/`do`) and hands back each /// region's fresh entry block for the caller to emit into and terminate. A trailing `; empty name…` /// clause sets a region the op's shape requires but this method leaves bodiless — an `if` with no @@ -175,19 +168,20 @@ impl<'slice, T, const N: usize> IntoOds<&'slice [T]> for &'slice [T; N] { /// A declaration names the receiver, the method and its typed parameters, the disposition, the /// operation, and the builder setter chain. Every setter argument is a parameter, the receiver /// `self`, or a closed keyword from the `@arg` rules; keywords are call-shaped, so a bare -/// identifier is always a parameter. The operation slot may be `checked(CheckedOp, UncheckedOp)`, -/// which threads a `checked: bool` selector. +/// identifier is always a parameter. /// /// A `base | flagged (…) … { … } flagged .setter ;` declaration stamps a pair of methods off one /// chain: `base` omits the unit-flag setter and `flagged` appends `.setter(unit_flag)`, so a binary /// mode is two named methods rather than one method taking a `bool`. /// /// Dispositions: `-> value` / `-> place` append at the `current_block()` cursor and wrap the single -/// result; `-> value nop_if_same(param)` short-circuits when the receiver already has that type; an -/// arrowless declaration is value-less and appends to the receiver block for a `Block` method, or at -/// the `current_block()` cursor for a `Value` / `Place`. A `Block` declaration listing region names -/// after `;` opens a region-bearing op and returns each region's entry block, or the sole block when -/// one region is named. Every argument is routed through [`IntoOds`] to the setter's type. +/// result; `-> value nop_if_same(param)` short-circuits when the receiver already has that type; +/// `-> value checked(CheckedOp)` threads a `checked: bool` selector that builds `CheckedOp` in place +/// of the declared operation off the same chain; an arrowless declaration is value-less and appends +/// to the receiver block for a `Block` method, or at the `current_block()` cursor for a `Value` / +/// `Place`. A `Block` declaration listing region names after `;` opens a region-bearing op and +/// returns each region's entry block, or the sole block when one region is named. Every argument is +/// routed through [`IntoOds`] to the setter's type. macro_rules! sol_ops { () => {}; @@ -314,6 +308,18 @@ macro_rules! sol_ops { .into() }; + (@flag_ty $checked_op:ident) => { bool }; + (@op [$context:ident] [$receiver:tt] [$flag:ident] checked($checked_op:ident) $operation:ident $($chain:tt)*) => { + if $flag { + sol_ops!(@build [$context] [$receiver] $checked_op $($chain)*) + } else { + sol_ops!(@build [$context] [$receiver] $operation $($chain)*) + } + }; + (@op [$context:ident] [$receiver:tt] [$flag:ident] $operation:ident $($chain:tt)*) => { + sol_ops!(@build [$context] [$receiver] $operation $($chain)*) + }; + (@disp_ty value) => { $crate::Value<'context> }; (@disp_ty place) => { $crate::Place<'context> }; (@disp_ty values) => { ::std::vec::Vec<$crate::Value<'context>> }; @@ -381,37 +387,15 @@ macro_rules! sol_ops { ( $receiver:ident :: $method:ident (self $(, $argument:ident : $kind:ident)* $(,)?) - -> value { checked($checked_op:ident, $unchecked_op:ident) $($chain:tt)* } - $($rest:tt)* - ) => { - impl<'context> $receiver<'context> { - pub fn $method( - self, - $($argument: sol_ops!(@ty $kind),)* - checked: bool, - context: &$crate::Context<'context>, - ) -> $crate::Value<'context> { - let receiver = self; - let operation = if checked { - sol_ops!(@build [context] [receiver] $checked_op $($chain)*) - } else { - sol_ops!(@build [context] [receiver] $unchecked_op $($chain)*) - }; - sol_ops!(@emit value [context] operation, "checked arithmetic op produces one result") - } - } - sol_ops!($($rest)*); - }; - - ( - $receiver:ident :: $method:ident (self $(, $argument:ident : $kind:ident)* $(,)?) - -> $disposition:ident $(nop_if_same($same:ident))? { $operation:ident $($chain:tt)* } + -> $disposition:ident $(nop_if_same($same:ident))? $(checked($checked_op:ident))? + { $operation:ident $($chain:tt)* } $($rest:tt)* ) => { impl<'context> $receiver<'context> { pub fn $method( self, $($argument: sol_ops!(@ty $kind),)* + $(checked: sol_ops!(@flag_ty $checked_op),)? context: &$crate::Context<'context>, ) -> sol_ops!(@disp_ty $disposition) { let receiver = self; @@ -419,7 +403,8 @@ macro_rules! sol_ops { return receiver.into(); })? sol_ops!(@emit $disposition [context] - sol_ops!(@build [context] [receiver] $operation $($chain)*), + sol_ops!(@op [context] [receiver] [checked] + $(checked($checked_op))? $operation $($chain)*), concat!(stringify!($operation), " produces one result")) } } diff --git a/solx-mlir/tests/lit/array_string_ops.sol b/solx-mlir/tests/lit/array_string_ops.sol index 9c19bcb27..ae6227fa2 100644 --- a/solx-mlir/tests/lit/array_string_ops.sol +++ b/solx-mlir/tests/lit/array_string_ops.sol @@ -9,6 +9,10 @@ // CHECK: sol.push %{{.*}} : !sol.array -> !sol.ptr // CHECK-NOT: sol.store +// CHECK: sol.func {{.*}}pushEmptyBraces +// CHECK: sol.push %{{.*}} : !sol.array -> !sol.ptr +// CHECK-NOT: sol.store + // CHECK: sol.func {{.*}}pushAssign // CHECK: sol.push %{{.*}} : !sol.array -> !sol.ptr // CHECK: sol.store %{{.*}}, %{{.*}} : ui256, !sol.ptr @@ -28,6 +32,9 @@ // CHECK: sol.func {{.*}}popLast // CHECK: sol.pop %{{.*}} : !sol.array +// CHECK: sol.func {{.*}}popEmptyBraces +// CHECK: sol.pop %{{.*}} : !sol.array + // CHECK: sol.func {{.*}}popByte // CHECK: sol.pop %{{.*}} : !sol.string @@ -46,6 +53,10 @@ contract C { arr.push(); } + function pushEmptyBraces() public { + arr.push({}); + } + function pushAssign(uint256 x) public { arr.push() = x; } @@ -66,6 +77,10 @@ contract C { arr.pop(); } + function popEmptyBraces() public { + arr.pop({}); + } + function popByte() public { data.pop(); } diff --git a/solx-mlir/tests/lit/evaluation_order/named_arguments.sol b/solx-mlir/tests/lit/evaluation_order/named_arguments.sol new file mode 100644 index 000000000..228cf632c --- /dev/null +++ b/solx-mlir/tests/lit/evaluation_order/named_arguments.sol @@ -0,0 +1,25 @@ +// RUN: solx --emit-mlir=sol %evaluation_order/named_arguments.sol | FileCheck %s + +// solc print-init spells an internal callee `@t_28` rather than `@"t(uint256)_28"`, so the symbol +// CHECKs are solx-only. The behavior is pinned against legacy by +// tests/solidity/simple/evaluation_order/named_arguments.sol. + +// CHECK: sol.func @{{.*call.*}} +// CHECK: sol.constant 1 : ui8 +// CHECK: %[[A:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.constant 2 : ui8 +// CHECK: %[[B:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.constant 3 : ui8 +// CHECK: %[[C:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.call @{{.*triple.*}}(%[[A]], %[[B]], %[[C]]) + +// CHECK: sol.func @{{.*struct_constructor.*}} +// CHECK: sol.constant 1 : ui8 +// CHECK: %[[FIELD_A:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.store %[[FIELD_A]] +// CHECK: sol.constant 2 : ui8 +// CHECK: %[[FIELD_B:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.store %[[FIELD_B]] +// CHECK: sol.constant 3 : ui8 +// CHECK: %[[FIELD_C:.*]] = sol.call @"t(uint256)_{{[0-9]+}}" +// CHECK: sol.store %[[FIELD_C]] diff --git a/solx-mlir/tests/lit/events.sol b/solx-mlir/tests/lit/events.sol index 7a5f8bb61..0ea69a049 100644 --- a/solx-mlir/tests/lit/events.sol +++ b/solx-mlir/tests/lit/events.sol @@ -7,6 +7,12 @@ // CHECK: %[[AMT:.*]] = sol.load // CHECK: sol.emit "Transfer(address,address,uint256)" indexed = [%[[CALLER]], %[[TO]]] non_indexed = [%[[AMT]]] : !sol.address, !sol.address, ui256 +// CHECK: sol.func @{{.*}}fireNamed +// CHECK: %[[CALLER:.*]] = sol.caller +// CHECK: %[[TO:.*]] = sol.load +// CHECK: %[[AMT:.*]] = sol.load +// CHECK: sol.emit "Transfer(address,address,uint256)" indexed = [%[[CALLER]], %[[TO]]] non_indexed = [%[[AMT]]] : !sol.address, !sol.address, ui256 + // CHECK: sol.func @{{.*}}fireAnon // CHECK: sol.emit non_indexed = [%{{.*}}] : ui256 @@ -18,6 +24,10 @@ contract C { emit Transfer(msg.sender, to, amount); } + function fireNamed(address to, uint256 amount) public { + emit Transfer({value: amount, to: to, from: msg.sender}); + } + function fireAnon(uint256 v) public { emit Anon(v); } diff --git a/solx-mlir/tests/lit/function_calls.sol b/solx-mlir/tests/lit/function_calls.sol index 864980b6b..f1ddd261f 100644 --- a/solx-mlir/tests/lit/function_calls.sol +++ b/solx-mlir/tests/lit/function_calls.sol @@ -23,6 +23,19 @@ // CHECK: sol.cast %{{.*}} : ui8 to ui256 // CHECK: sol.call @{{.*add.*}} +// CHECK: sol.func @{{.*named_argument.*}} +// CHECK: sol.load %{{.*}} +// CHECK: sol.constant 1 : ui8 +// CHECK: sol.call @{{.*add.*}} + +// CHECK: sol.func @{{.*paren_named_argument.*}} +// CHECK: %[[X:.*]] = sol.load +// CHECK: %[[ONE:.*]] = sol.cast +// CHECK: sol.icall %{{.*}}(%[[X]], %[[ONE]]) + +// CHECK: sol.func @{{.*empty_braces.*}} +// CHECK: sol.call @{{.*literal_argument.*}}() : () -> () + // CHECK: sol.func @{{.*tuple_statement.*}} // CHECK: sol.call @{{.*add.*}} // CHECK: sol.call @{{.*double.*}} @@ -50,6 +63,18 @@ contract C { return add(a, b); } + function named_argument(uint256 x) public pure returns (uint256) { + return add({b: 1, a: x}); + } + + function paren_named_argument(uint256 x) public pure returns (uint256) { + return (add)({b: 1, a: x}); + } + + function empty_braces() public pure { + literal_argument({}); + } + function tuple_statement(uint256 x) public pure { (add(x, x), double(x)); } diff --git a/solx-mlir/tests/lit/function_overloads.sol b/solx-mlir/tests/lit/function_overloads.sol index a00760cd5..5b733674f 100644 --- a/solx-mlir/tests/lit/function_overloads.sol +++ b/solx-mlir/tests/lit/function_overloads.sol @@ -4,6 +4,11 @@ // CHECK: sol.func @{{.*pick.*}}(%{{.*}}: ui256) -> ui256 // CHECK: sol.func @{{.*pick.*}}(%{{.*}}: i1) -> i1 +// CHECK: sol.func @{{.*pick_named.*}} +// CHECK: sol.call @{{.*pick.*}}(%{{.*}}) : (ui256) -> ui256 +// CHECK: sol.func @{{.*pick_named_bool.*}} +// CHECK: sol.call @{{.*pick.*}}(%{{.*}}) : (i1) -> i1 + // CHECK: sol.func @{{.*sum.*}}(%{{.*}}: ui256) -> ui256 // CHECK: sol.func @{{.*sum.*}}(%{{.*}}: ui256, %{{.*}}: ui256) -> ui256 @@ -32,6 +37,14 @@ contract C { return b; } + function pick_named() public pure returns (uint256) { + return pick({x: 1}); + } + + function pick_named_bool() public pure returns (bool) { + return pick({b: true}); + } + function sum(uint256 x) internal pure returns (uint256) { return x; } diff --git a/solx-mlir/tests/lit/internal_function_pointer.sol b/solx-mlir/tests/lit/internal_function_pointer.sol index c87667c1a..ba6612d3a 100644 --- a/solx-mlir/tests/lit/internal_function_pointer.sol +++ b/solx-mlir/tests/lit/internal_function_pointer.sol @@ -12,6 +12,10 @@ // CHECK: %[[POINTER:.*]] = sol.load %[[SLOT]] : !sol.ptr ui256>, Stack>, !sol.func_ref<() -> ui256> // CHECK: sol.icall %[[POINTER]]() : !sol.func_ref<() -> ui256>, () -> ui256 +// CHECK: sol.func @{{.*run_empty_braces.*}} +// CHECK: sol.func_constant @{{.*g.*}} : !sol.func_ref<() -> ui256> +// CHECK: sol.icall %{{[0-9]+}}() : !sol.func_ref<() -> ui256>, () -> ui256 + // CHECK: sol.func @{{.*invoke.*}}(%[[ARGUMENT:.*]]: !sol.func_ref<() -> ui256>) -> ui256 // CHECK: sol.store %[[ARGUMENT]], %[[SLOT:.*]] : !sol.func_ref<() -> ui256>, !sol.ptr ui256>, Stack> // CHECK: %[[POINTER:.*]] = sol.load %[[SLOT]] : !sol.ptr ui256>, Stack>, !sol.func_ref<() -> ui256> @@ -76,6 +80,11 @@ contract C { return functionPointer(); } + function run_empty_braces() public returns (uint256) { + function () internal returns (uint256) functionPointer = g; + return functionPointer({}); + } + function invoke(function () internal returns (uint256) f) internal returns (uint256) { return f(); } diff --git a/solx-mlir/tests/lit/revert.sol b/solx-mlir/tests/lit/revert.sol index 6867269ff..9a4000fd6 100644 --- a/solx-mlir/tests/lit/revert.sol +++ b/solx-mlir/tests/lit/revert.sol @@ -4,12 +4,19 @@ // CHECK: sol.func @{{.*plain_revert.*}} // CHECK: sol.revert{{$}} +// CHECK: sol.func @{{.*empty_named_revert.*}} +// CHECK: sol.revert{{$}} + // CHECK: sol.func @{{.*message_revert.*}} // CHECK: sol.revert "oops" // CHECK: sol.func @{{.*empty_message_revert.*}} // CHECK: sol.revert "" +// CHECK: sol.func @{{.*runtime_message_revert.*}} +// CHECK: %[[MESSAGE:.*]] = sol.data_loc_cast %{{.*}} : !sol.string, !sol.string +// CHECK: sol.revert "Error(string)" %[[MESSAGE]] : !sol.string {call} + // CHECK: sol.func @{{.*custom_error.*}} // CHECK: sol.revert "TooLow(uint256,uint256)" %{{.*}}, %{{.*}} : ui256, ui256 {call} @@ -25,6 +32,10 @@ contract C { revert(); } + function empty_named_revert() public pure { + revert({}); + } + function message_revert() public pure { revert("oops"); } @@ -33,6 +44,10 @@ contract C { revert(""); } + function runtime_message_revert(string calldata message) public pure { + revert(message); + } + function custom_error(uint256 x) public pure { revert TooLow(x, 100); } diff --git a/solx-mlir/tests/lit/struct_constructor.sol b/solx-mlir/tests/lit/struct_constructor.sol index bdc04d84b..516644ced 100644 --- a/solx-mlir/tests/lit/struct_constructor.sol +++ b/solx-mlir/tests/lit/struct_constructor.sol @@ -10,6 +10,19 @@ // CHECK: sol.gep %{{.*}}, %{{.*}} : !sol.struct<(ui256, ui256), Memory>, ui64, !sol.ptr // CHECK: sol.store %{{.*}}, %{{.*}} : ui256, !sol.ptr +// CHECK: sol.func {{.*}}build_named{{.*}}-> !sol.struct<(ui256, ui256), Memory> +// CHECK: %[[STRUCT:.*]] = sol.malloc :{{ +}}!sol.struct<(ui256, ui256), Memory> +// CHECK: %[[FIRST:.*]] = sol.constant 0 : ui64 +// CHECK: %[[A:.*]] = sol.gep %[[STRUCT]], %[[FIRST]] : !sol.struct<(ui256, ui256), Memory>, ui64, !sol.ptr +// CHECK: %[[ONE:.*]] = sol.constant 1 : ui8 +// CHECK: %[[A_VALUE:.*]] = sol.cast %[[ONE]] : ui8 to ui256 +// CHECK: sol.store %[[A_VALUE]], %[[A]] : ui256, !sol.ptr +// CHECK: %[[SECOND:.*]] = sol.constant 1 : ui64 +// CHECK: %[[B:.*]] = sol.gep %[[STRUCT]], %[[SECOND]] : !sol.struct<(ui256, ui256), Memory>, ui64, !sol.ptr +// CHECK: %[[TWO:.*]] = sol.constant 2 : ui8 +// CHECK: %[[B_VALUE:.*]] = sol.cast %[[TWO]] : ui8 to ui256 +// CHECK: sol.store %[[B_VALUE]], %[[B]] : ui256, !sol.ptr + // CHECK: sol.func {{.*}}build_tagged{{.*}}-> !sol.struct<(!sol.fixedbytes<4>, ui256), Memory> // CHECK: sol.constant {{.*}} : ui32 // CHECK: sol.bytes_cast %{{.*}} : ui32 to !sol.fixedbytes<4> @@ -23,6 +36,10 @@ contract C { return S(x, y); } + function build_named() public pure returns (S memory) { + return S({b: 2, a: 1}); + } + function build_tagged() public pure returns (T memory) { return T("abcd", 7); } diff --git a/solx-slang/src/contract/function/expression/arithmetic.rs b/solx-slang/src/contract/function/expression/arithmetic.rs index cc3a741f4..96b14f491 100644 --- a/solx-slang/src/contract/function/expression/arithmetic.rs +++ b/solx-slang/src/contract/function/expression/arithmetic.rs @@ -6,9 +6,9 @@ use slang_solidity_v2::ast::AdditiveExpression; use slang_solidity_v2::ast::AdditiveExpressionOperator; use slang_solidity_v2::ast::ExponentiationExpression; +use slang_solidity_v2::ast::Expression; use slang_solidity_v2::ast::MultiplicativeExpression; use slang_solidity_v2::ast::MultiplicativeExpressionOperator; -use slang_solidity_v2::ast::PositionalArguments; use slang_solidity_v2::ast::UserDefinedOperatorExpression; use solx_mlir::Context; @@ -58,7 +58,7 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// legacy, then combined by `operator`. pub fn modular( &mut self, - arguments: &PositionalArguments, + arguments: &[Expression], operator: impl FnOnce( Value<'context>, Value<'context>, @@ -67,7 +67,6 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' ) -> Value<'context>, ) -> Value<'context> { let field = MlirType::field(self.melior); - let arguments: Vec<_> = arguments.iter().collect(); let modulus = self.converted(&arguments[2], field); let right = self.converted(&arguments[1], field); let left = self.converted(&arguments[0], field); diff --git a/solx-slang/src/contract/function/expression/call/arguments.rs b/solx-slang/src/contract/function/expression/call/arguments.rs index 8e26292fa..2da2017ad 100644 --- a/solx-slang/src/contract/function/expression/call/arguments.rs +++ b/solx-slang/src/contract/function/expression/call/arguments.rs @@ -7,9 +7,9 @@ use std::collections::HashMap; use slang_solidity_v2::ast::ArgumentsDeclaration; use slang_solidity_v2::ast::Expression; use slang_solidity_v2::ast::NamedArguments; +use slang_solidity_v2::ast::NodeId; use slang_solidity_v2::ast::Parameter; use slang_solidity_v2::ast::Parameters; -use slang_solidity_v2::ast::PositionalArguments; use solx_mlir::Value; @@ -25,7 +25,10 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' ) -> Vec<(Parameter, Value<'context>)> { let ordered: Vec = match arguments { ArgumentsDeclaration::PositionalArguments(positional) => positional.iter().collect(), - ArgumentsDeclaration::NamedArguments(named) => Self::named_arguments(named, parameters), + ArgumentsDeclaration::NamedArguments(named) => Self::named_arguments( + named, + parameters.iter().map(|parameter| parameter.node_id()), + ), }; parameters .iter() @@ -39,27 +42,36 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' } /// The positional argument list of a call, each argument evaluated in order. - pub fn positional_arguments(&mut self, node: &PositionalArguments) -> Vec> { - node.iter() - .map(|argument| self.expression(&argument)) + pub fn positional_arguments(&mut self, arguments: &[Expression]) -> Vec> { + arguments + .iter() + .map(|argument| self.expression(argument)) .collect() } - /// The named argument list of a call, reordered into the definition's parameter order by the - /// name matching slang has validated is total and unambiguous. - fn named_arguments(named: &NamedArguments, parameters: &Parameters) -> Vec { - let mut by_name: HashMap = named + /// The named argument list of a call, reordered into the targets' declaration order through + /// the label bindings slang resolves. + pub fn named_arguments( + named: &NamedArguments, + targets: impl Iterator, + ) -> Vec { + let mut by_target: HashMap = named .iter() - .map(|argument| (argument.name().name().to_owned(), argument.value())) + .map(|argument| { + ( + argument + .name() + .resolve_to_definition() + .expect("slang binds every named-argument label") + .node_id(), + argument.value(), + ) + }) .collect(); - parameters - .iter() - .map(|parameter| { - let identifier = parameter - .name() - .expect("slang validates a named argument targets a named parameter"); - by_name - .remove(identifier.name()) + targets + .map(|target| { + by_target + .remove(&target) .expect("slang validates every parameter receives a named argument") }) .collect() diff --git a/solx-slang/src/contract/function/expression/call/mod.rs b/solx-slang/src/contract/function/expression/call/mod.rs index 4d26ebf33..07129df95 100644 --- a/solx-slang/src/contract/function/expression/call/mod.rs +++ b/solx-slang/src/contract/function/expression/call/mod.rs @@ -11,8 +11,8 @@ use slang_solidity_v2::ast::Definition; use slang_solidity_v2::ast::Expression; use slang_solidity_v2::ast::FunctionCallExpression; use slang_solidity_v2::ast::FunctionDefinition; +use slang_solidity_v2::ast::FunctionType; use slang_solidity_v2::ast::MemberAccessExpression; -use slang_solidity_v2::ast::PositionalArguments; use slang_solidity_v2::ast::StructDefinition; use slang_solidity_v2::ast::Type; @@ -40,11 +40,11 @@ pub enum Call { /// A direct call to a named function. Function(FunctionDefinition), /// A call through a function-typed value, dispatched by `sol.icall`. - FunctionPointer, + FunctionPointer(FunctionType), } impl Call { - /// The canonical signature ABI-encoding a runtime `require` message. + /// The canonical signature ABI-encoding a runtime `require` or `revert` message. const ERROR_STRING_SIGNATURE: &'static str = "Error(string)"; /// Classifies and emits `node`, routing each kind to its emission and returning its results in @@ -53,24 +53,23 @@ impl Call { node: &FunctionCallExpression, scope: &mut FunctionScope<'_, '_, 'context>, ) -> Vec> { - let ArgumentsDeclaration::PositionalArguments(arguments) = &node.arguments() else { - unreachable!("only positional arguments supported"); - }; - match Self::from_call(node) { + let kind = Self::from_call(node); + let arguments = kind.arguments(node); + match kind { Self::StructConstruction(struct_definition) => { - Self::struct_construction(&struct_definition, node, arguments, scope) + Self::struct_construction(&struct_definition, node, &arguments, scope) } - Self::TypeConversion => vec![Self::type_conversion(node, arguments, scope)], - Self::Builtin(built_in) => Self::builtin(built_in, arguments, scope) + Self::TypeConversion => vec![Self::type_conversion(node, &arguments, scope)], + Self::Builtin(built_in) => Self::builtin(built_in, &arguments, scope) .into_iter() .collect(), - Self::Member(access) => Self::member(&access, node, arguments, scope) + Self::Member(access) => Self::member(&access, node, &arguments, scope) .into_iter() .collect(), - Self::Function(function_definition) => { - scope.call(&function_definition, arguments.iter()) + Self::Function(function_definition) => scope.call(&function_definition, arguments), + Self::FunctionPointer(function_type) => { + Self::function_pointer(&function_type, &node.operand(), &arguments, scope) } - Self::FunctionPointer => Self::function_pointer(node, arguments, scope), } } @@ -101,8 +100,8 @@ impl Call { { return Self::Function(function_definition); } - if matches!(identifier.get_type(), Some(Type::Function(_))) { - return Self::FunctionPointer; + if let Some(Type::Function(function_type)) = identifier.get_type() { + return Self::FunctionPointer(function_type); } unimplemented!("unsupported callee '{}'", identifier.name()) } @@ -110,17 +109,64 @@ impl Call { if matches!( access.member().resolve_to_definition(), Some(Definition::StructMember(_)) - ) && matches!(access.get_type(), Some(Type::Function(_))) + ) && let Some(Type::Function(function_type)) = access.get_type() { - return Self::FunctionPointer; + return Self::FunctionPointer(function_type); } Self::Member(access) } - callee if matches!(callee.get_type(), Some(Type::Function(_))) => Self::FunctionPointer, - callee => unimplemented!( - "unsupported callee expression: {:?}", - std::mem::discriminant(&callee) - ), + callee => match callee.get_type() { + Some(Type::Function(function_type)) => Self::FunctionPointer(function_type), + _ => unimplemented!( + "unsupported callee expression: {:?}", + std::mem::discriminant(&callee) + ), + }, + } + } + + /// The call's arguments in the callee's declaration order, which is the order the named form + /// evaluates in. A kind that declares no parameters orders against nothing, so the empty + /// braces of `f({})` are its only named form. + fn arguments(&self, call: &FunctionCallExpression) -> Vec { + match call.arguments() { + ArgumentsDeclaration::PositionalArguments(positional) => positional.iter().collect(), + ArgumentsDeclaration::NamedArguments(named) => match self { + Self::StructConstruction(struct_definition) => FunctionScope::named_arguments( + &named, + struct_definition + .members() + .iter() + .map(|member| member.node_id()), + ), + Self::Function(function_definition) => FunctionScope::named_arguments( + &named, + function_definition + .parameters() + .iter() + .map(|parameter| parameter.node_id()), + ), + Self::FunctionPointer(function_type) => match function_type.associated_definition() + { + Some(Definition::Function(function_definition)) => { + FunctionScope::named_arguments( + &named, + function_definition + .parameters() + .iter() + .map(|parameter| parameter.node_id()), + ) + } + _ if named.is_empty() => Vec::new(), + _ => unreachable!("a function value declares no labeled parameter"), + }, + Self::Member(_) | Self::Builtin(_) if named.is_empty() => Vec::new(), + Self::Member(_) => unimplemented!("named arguments on a member callee"), + Self::Builtin(_) => unreachable!("a built-in declares no labeled parameter"), + Self::TypeConversion => { + unreachable!("a type conversion classifies only positional arguments") + } + }, } } @@ -129,14 +175,14 @@ impl Call { fn struct_construction<'context>( struct_definition: &StructDefinition, call: &FunctionCallExpression, - arguments: &PositionalArguments, + arguments: &[Expression], scope: &mut FunctionScope<'_, '_, 'context>, ) -> Vec> { let struct_address = Place::malloc(scope.typing(call.get_type()), scope); for (index, (member, argument)) in struct_definition .members() .iter() - .zip(arguments.iter()) + .zip(arguments) .enumerate() { let field_type = scope.resolve_type( @@ -144,7 +190,7 @@ impl Call { Some(solx_utils::DataLocation::Memory), ); let field_address = struct_address.gep_field(index, field_type, scope); - field_address.store(scope.converted(&argument, field_type), scope); + field_address.store(scope.converted(argument, field_type), scope); } vec![struct_address.into()] } @@ -153,15 +199,14 @@ impl Call { /// cast. fn type_conversion<'context>( call: &FunctionCallExpression, - arguments: &PositionalArguments, + arguments: &[Expression], scope: &mut FunctionScope<'_, '_, 'context>, ) -> Value<'context> { let operand = arguments - .iter() - .next() + .first() .expect("classification admits exactly one argument"); let target_type = scope.typing(call.get_type()); - scope.converted(&operand, target_type) + scope.converted(operand, target_type) } /// Statement-style built-ins (`assert`, `require`, `revert`) produce no value. @@ -170,16 +215,15 @@ impl Call { /// evaluates at runtime and is ABI-encoded under the `Error(string)` selector via its call form. fn builtin<'context>( built_in: BuiltIn, - arguments: &PositionalArguments, + arguments: &[Expression], scope: &mut FunctionScope<'_, '_, 'context>, ) -> Option> { match built_in { BuiltIn::Assert => { let condition_expression = arguments - .iter() - .next() + .first() .expect("slang validates the arity of assert"); - let condition = scope.expression(&condition_expression).is_nonzero(scope); + let condition = scope.expression(condition_expression).is_nonzero(scope); scope.current_block().assert(condition, scope); None } @@ -187,7 +231,7 @@ impl Call { let mut iter = arguments.iter(); let condition_expression = iter.next().expect("slang validates the arity of require"); - let condition = scope.expression(&condition_expression).is_nonzero(scope); + let condition = scope.expression(condition_expression).is_nonzero(scope); let (values, message, custom) = match iter.next() { Some(Expression::StringExpression(string_expression)) => ( Vec::new(), @@ -200,7 +244,7 @@ impl Call { Some(expression) => { let string_memory_type = MlirType::string(scope.melior, solx_utils::DataLocation::Memory); - let message_value = scope.converted(&expression, string_memory_type); + let message_value = scope.converted(expression, string_memory_type); ( vec![message_value], Some(Self::ERROR_STRING_SIGNATURE.to_owned()), @@ -224,15 +268,24 @@ impl Call { None } BuiltIn::Revert => { - let message = match arguments.iter().next() { - Some(Expression::StringExpression(string_expression)) => Some( - String::from_utf8(string_expression.value()) - .expect("slang validates string lals are UTF-8"), - ), - Some(_) => unreachable!("revert message is a string lal"), - None => None, - }; - scope.current_block().revert(message.as_deref(), &[], scope); + match arguments.first() { + Some(Expression::StringExpression(string_expression)) => { + let message = String::from_utf8(string_expression.value()) + .expect("slang validates string literals are UTF-8"); + scope.current_block().revert(Some(&message), &[], scope); + } + Some(expression) => { + let string_type = + MlirType::string(scope.melior, solx_utils::DataLocation::Memory); + let message = scope.converted(expression, string_type); + scope.current_block().revert_custom( + Some(Self::ERROR_STRING_SIGNATURE), + &[message], + scope, + ); + } + None => scope.current_block().revert(None, &[], scope), + } None } BuiltIn::Gasleft => Some(Value::gas_left(scope)), @@ -284,7 +337,7 @@ impl Call { fn member<'context>( access: &MemberAccessExpression, call: &FunctionCallExpression, - arguments: &PositionalArguments, + arguments: &[Expression], scope: &mut FunctionScope<'_, '_, 'context>, ) -> Option> { match access.member().resolve_to_built_in() { @@ -342,14 +395,13 @@ impl Call { scope, ); let values = iter - .map(|argument| scope.expression(&argument)) + .map(|argument| scope.expression(argument)) .collect::>(); Some(Value::encode(&values, Some(selector), scope)) } Some(BuiltIn::AbiDecode) => { let payload_expression = arguments - .iter() - .next() + .first() .expect("slang validates the payload argument"); let return_slang_type = call .get_type() @@ -358,7 +410,7 @@ impl Call { unimplemented!("abi.decode returning multiple values is not yet supported"); } Some(Value::decode( - scope.expression(&payload_expression), + scope.expression(payload_expression), scope.resolve_type(&return_slang_type, None), scope, )) @@ -372,11 +424,11 @@ impl Call { let base_slang_type = base .get_type() .expect("base of array push has a resolved type"); - let value_argument = arguments.iter().next(); + let value_argument = arguments.first(); let (place, _) = scope.expression_place(&base); if let Type::Bytes(_) = &base_slang_type - && let Some(value_argument) = &value_argument + && let Some(value_argument) = value_argument { let appended = scope.converted( value_argument, @@ -411,7 +463,7 @@ impl Call { let Some(value_argument) = value_argument else { return Some(new_slot); }; - Place::from(new_slot).store(scope.converted(&value_argument, element_type), scope); + Place::from(new_slot).store(scope.converted(value_argument, element_type), scope); None } Some(BuiltIn::BytesConcat | BuiltIn::StringConcat) => { @@ -427,20 +479,17 @@ impl Call { /// The signature comes from the callee's binder type: a pointer callee names no definition to /// look a registered signature up by. fn function_pointer<'context>( - call: &FunctionCallExpression, - arguments: &PositionalArguments, + function_type: &FunctionType, + callee: &Expression, + arguments: &[Expression], scope: &mut FunctionScope<'_, '_, 'context>, ) -> Vec> { - let callee = call.operand(); - let Some(Type::Function(function_type)) = callee.get_type() else { - unreachable!("classification admits only function-typed callees"); - }; - let function_type = scope.contract.source_unit.function_type(&function_type); - let pointer = scope.expression(&callee); + let function_type = scope.contract.source_unit.function_type(function_type); + let pointer = scope.expression(callee); let converted: Vec> = arguments .iter() .zip(&function_type.parameters) - .map(|(argument, ¶meter_type)| scope.converted(&argument, parameter_type)) + .map(|(argument, ¶meter_type)| scope.converted(argument, parameter_type)) .collect(); pointer.indirect_call(&converted, &function_type.results, scope) } diff --git a/solx-slang/src/contract/function/statement/variable_declaration.rs b/solx-slang/src/contract/function/statement/variable_declaration.rs index 0c0332d50..699ecc7ed 100644 --- a/solx-slang/src/contract/function/statement/variable_declaration.rs +++ b/solx-slang/src/contract/function/statement/variable_declaration.rs @@ -30,8 +30,7 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' } /// A single-typed variable declaration. An explicit initializer is evaluated before the slot is - /// allocated, matching solc's order; an absent one default-initializes the slot to the type's - /// default value. + /// allocated; an absent one default-initializes the slot to the type's default value. pub fn single_typed_declaration(&mut self, node: &SingleTypedDeclaration) { let declared_type = self.typing(node.declaration().get_type()); match node.value() { diff --git a/solx-slang/src/contract/storage_slot.rs b/solx-slang/src/contract/storage_slot.rs index 7e74cb17c..2fd4cc740 100644 --- a/solx-slang/src/contract/storage_slot.rs +++ b/solx-slang/src/contract/storage_slot.rs @@ -8,9 +8,8 @@ use slang_solidity_v2::abi::StorageItem; /// Storage location of a state variable in contract storage. #[derive(Debug, Clone)] pub struct StorageSlot { - /// MLIR symbol name, formatted as `{label}_{node_id}` to match solc. - /// The slang AST node id disambiguates like-named variables across - /// inherited contracts. + /// MLIR symbol name, formatted as `{label}_{node_id}`. The slang AST node + /// id disambiguates like-named variables across inherited contracts. pub name: String, /// 256-bit storage slot index. pub slot: U256, diff --git a/solx-slang/src/scope/function.rs b/solx-slang/src/scope/function.rs index 04657c691..682ae5757 100644 --- a/solx-slang/src/scope/function.rs +++ b/solx-slang/src/scope/function.rs @@ -46,7 +46,7 @@ impl<'contract, 'source_unit, 'context> FunctionScope<'contract, 'source_unit, ' /// Binds a named local: allocates its stack pointer, stores the value its initializer yields, /// and defines the binding in the current scope. The initializer runs after the allocation so - /// the slot precedes the value that initializes it, matching solc's emission order. + /// the slot precedes the value that initializes it. pub fn define_local( &mut self, name: &str, diff --git a/solx-slang/src/source_unit.rs b/solx-slang/src/source_unit.rs index 629109358..48303e18f 100644 --- a/solx-slang/src/source_unit.rs +++ b/solx-slang/src/source_unit.rs @@ -20,11 +20,11 @@ use crate::scope::source_unit::SourceUnitScope; impl<'context> SourceUnitScope<'context> { /// Lowers every contract the unit deploys into standard-JSON contract outputs keyed by contract - /// name, each in its own MLIR module off the file's melior context, as solc emits one module per - /// contract. An abstract contract and an interface deploy nothing and produce no module. A - /// contract with a contract base is skipped because emission collects only the contract's own - /// state and functions: an interface base carries nothing to inherit, a contract base carries - /// state and bodies that would be silently dropped. + /// name, each in its own MLIR module off the file's melior context. An abstract contract and an + /// interface deploy nothing and produce no module. A contract with a contract base is skipped + /// because emission collects only the contract's own state and functions: an interface base + /// carries nothing to inherit, a contract base carries state and bodies that would be silently + /// dropped. /// /// # Errors /// diff --git a/solx/tests/cli/stack_too_deep.rs b/solx/tests/cli/stack_too_deep.rs index e9afaeaa5..fb53b9350 100644 --- a/solx/tests/cli/stack_too_deep.rs +++ b/solx/tests/cli/stack_too_deep.rs @@ -2,6 +2,7 @@ //! CLI tests for stack-too-deep handling. //! +#[cfg(feature = "solc")] use predicates::prelude::*; #[cfg(feature = "solc")] diff --git a/tests/solidity/simple/constant_expressions/bitwise.sol b/tests/solidity/simple/constant_expressions/bitwise.sol index d6ab1ab97..c4121cee2 100644 --- a/tests/solidity/simple/constant_expressions/bitwise.sol +++ b/tests/solidity/simple/constant_expressions/bitwise.sol @@ -1,4 +1,4 @@ -//! { "cases": [ { +//! { "ignore": true, "cases": [ { //! "name": "test", //! "inputs": [ //! { diff --git a/tests/solidity/simple/evaluation_order/named_arguments.sol b/tests/solidity/simple/evaluation_order/named_arguments.sol new file mode 100644 index 000000000..4f09d99fa --- /dev/null +++ b/tests/solidity/simple/evaluation_order/named_arguments.sol @@ -0,0 +1,44 @@ +//! { "modes": [ "E" ], "cases": [ { +//! "name": "call", +//! "inputs": [ { "method": "call", "calldata": [] } ], +//! "expected": [ "123", "123" ] +//! }, { +//! "name": "struct_constructor", +//! "inputs": [ { "method": "struct_constructor", "calldata": [] } ], +//! "expected": [ "123", "123" ] +//! } ] } + +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.0; + +contract Test { + struct S { + uint256 a; + uint256 b; + uint256 c; + } + + uint256 order; + + function t(uint256 n) internal returns (uint256) { + order = order * 10 + n; + return n; + } + + function triple(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) { + return a * 100 + b * 10 + c; + } + + function call() public returns (uint256, uint256) { + order = 0; + uint256 result = triple({c: t(3), a: t(1), b: t(2)}); + return (result, order); + } + + function struct_constructor() public returns (uint256, uint256) { + order = 0; + S memory s = S({c: t(3), a: t(1), b: t(2)}); + return (s.a * 100 + s.b * 10 + s.c, order); + } +} From 19cf43089c055a21a06f02c17864d9e236a6cdec Mon Sep 17 00:00:00 2001 From: Bas van Gijzel Date: Wed, 5 Aug 2026 12:50:28 +0000 Subject: [PATCH 10/11] ci(coverage): report solx-llvm fork coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second llvm-cov show/report/export over the same profdata, scoped via positional source paths to the fork dirs the LLVM team cares about (EVM backend, MLIR Sol/Yul dialects and conversions). Produces its own HTML artifact and a flagged (llvm-fork) Codecov upload, with one Codecov component per area. Also fixes collection along the way: the integration tester never set LLVM_PROFILE_FILE, so the thousands of spawned solx subprocesses — the main source of solx-llvm C++ counters — all clobbered a single default.profraw. It now uses the same %4m online-merge pool as the unit tests. And the job gets a timeout-minutes backstop, sized for a cold instrumented LLVM build. --- .github/workflows/coverage.yaml | 67 +++++++++++++++++++++++++++++++++ codecov.yml | 26 +++++++++++++ 2 files changed, 93 insertions(+) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 63ba05abc..e367df892 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -37,6 +37,10 @@ jobs: # summary comment step. if: ${{ github.event_name == 'push' || !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-24.04 + # Backstop against hung compiles (the tester drives a pipeline still in + # development); sized for a cold instrumented LLVM build when a solx-llvm + # bump lands before cache-warmup has run for that SHA. + timeout-minutes: 180 permissions: contents: read pull-requests: write @@ -56,7 +60,9 @@ jobs: env: PROFDATA_FILE: solx.profdata LCOV_FILE: codecov.lcov + LCOV_FILE_LLVM: codecov-llvm-fork.lcov OUTPUT_HTML_DIR: COVERAGE + OUTPUT_HTML_DIR_LLVM: COVERAGE-LLVM # ^/rustc/ = std sources monomorphized into our binaries; the remapped # path exists on no runner, so llvm-cov show errors trying to render it. COVERAGE_IGNORE_REGEX: '/(solx-llvm|target-llvm|solx-solidity|\.cargo|cargo/registry)/|^/rustc/' @@ -110,6 +116,11 @@ jobs: - name: Run integration tests env: SOLX_BINARY: ${{ steps.build-solx.outputs.binary-path }} + # %Nm without %p: subprocesses merge online into a pool of 4 files + # instead of clobbering a single default.profraw. The pool is what + # preserves the solx-llvm C++ counters, which only the spawned solx + # compile subprocesses exercise. + LLVM_PROFILE_FILE: 'integration-%4m.profraw' run: | TMP="${PROFDATA_FILE}.tmp" TARGET_DIR="./target/x86_64-unknown-linux-gnu/release" @@ -176,6 +187,37 @@ jobs: -instr-profile=${PROFDATA_FILE} \ "${OBJECTS[@]}" > ./llvm/${LCOV_FILE} + # solx-llvm fork coverage for the LLVM team: same profdata, restricted + # via positional source paths to the fork-specific dirs instead of the + # ignore regex that keeps them out of the solx report above. + - name: Generate LLVM fork coverage reports + run: | + # First binary is positional: with -object only, llvm-cov consumes + # the first positional SOURCE path as the covered binary. + OBJECTS=( + "${GITHUB_WORKSPACE}/solx-instrumented" + -object "${GITHUB_WORKSPACE}/solx-tester-instrumented" + -object "${GITHUB_WORKSPACE}/solx-dev-instrumented" + ) + FORK_PATHS=( + "${GITHUB_WORKSPACE}/solx-llvm/llvm/lib/Target/EVM" + "${GITHUB_WORKSPACE}/solx-llvm/mlir/lib/Dialect/Sol" + "${GITHUB_WORKSPACE}/solx-llvm/mlir/lib/Dialect/Yul" + "${GITHUB_WORKSPACE}/solx-llvm/mlir/lib/Conversion/SolToYul" + "${GITHUB_WORKSPACE}/solx-llvm/mlir/lib/Conversion/YulToStandard" + ) + # llvm-cov only warns on a nonexistent source path, silently dropping + # the area from every report; fail instead. + for path in "${FORK_PATHS[@]}"; do + [ -d "${path}" ] || { echo "::error::fork source dir missing: ${path}"; exit 1; } + done + llvm-cov show --show-directory-coverage \ + --format=html --output-dir="${OUTPUT_HTML_DIR_LLVM}" \ + -instr-profile="${PROFDATA_FILE}" "${OBJECTS[@]}" "${FORK_PATHS[@]}" + llvm-cov export --format=lcov \ + -instr-profile="${PROFDATA_FILE}" \ + "${OBJECTS[@]}" "${FORK_PATHS[@]}" > "./llvm/${LCOV_FILE_LLVM}" + - name: Coverage summary id: coverage-summary run: | @@ -258,6 +300,12 @@ jobs: name: 'Coverage integration tests HTML' path: ${{ env.OUTPUT_HTML_DIR }} + - name: Upload LLVM fork coverage artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: 'Coverage LLVM fork HTML' + path: ${{ env.OUTPUT_HTML_DIR_LLVM }} + - name: Upload coverage to Codecov uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 if: (success() || failure()) @@ -271,3 +319,22 @@ jobs: slug: ${{ github.repository }} # Surface upload failures instead of silently going green. fail_ci_if_error: true + + # Separate flagged upload so the fork coverage can be filtered apart + # from the solx crates in the Codecov UI (see component_management in + # codecov.yml for the per-area split). + - name: Upload LLVM fork coverage to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + if: (success() || failure()) + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: 'llvm/codecov-llvm-fork.lcov' + flags: llvm-fork + # Enumerate the network with `git ls-files --recurse-submodules` so + # solx-llvm paths survive server-side path fixing; without this the + # fork files are silently dropped at ingest (not in the git tree). + recurse_submodules: true + disable_search: true + plugins: noop + slug: ${{ github.repository }} + fail_ci_if_error: true diff --git a/codecov.yml b/codecov.yml index 14005c2c9..4799d1b62 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,3 +9,29 @@ coverage: default: false # disable the patch status that measures patch changes comment: false + +# solx-llvm fork coverage (uploaded under the `llvm-fork` flag) split by the +# areas the LLVM team cares about. Paths are as they appear in the lcov from +# coverage.yaml, i.e. relative to the solx checkout with the submodule prefix. +component_management: + individual_components: + - component_id: llvm-evm-backend + name: LLVM EVM backend + paths: + - solx-llvm/llvm/lib/Target/EVM/** + - component_id: mlir-sol-dialect + name: MLIR Sol dialect + paths: + - solx-llvm/mlir/lib/Dialect/Sol/** + - component_id: mlir-yul-dialect + name: MLIR Yul dialect + paths: + - solx-llvm/mlir/lib/Dialect/Yul/** + - component_id: mlir-sol-to-yul + name: MLIR SolToYul + paths: + - solx-llvm/mlir/lib/Conversion/SolToYul/** + - component_id: mlir-yul-to-standard + name: MLIR YulToStandard + paths: + - solx-llvm/mlir/lib/Conversion/YulToStandard/** From f20ad9c0ef6cd9572f9f078daa376d04488c490e Mon Sep 17 00:00:00 2001 From: Bas van Gijzel Date: Wed, 5 Aug 2026 12:51:13 +0000 Subject: [PATCH 11/11] ci(coverage): replace the custom PR comment with the Codecov app comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-rolled awk per-crate table (and its red/green/yellow emoji thresholds) duplicated what Codecov already renders, minus PR-delta context. Enable the Codecov app comment instead — same style as era-compiler-llvm and EDR — with flags and components in the layout so the solx/llvm-fork split and the per-area fork numbers stay visible on the PR. The comment step was also the only reason the job needed pull-requests: write. --- .github/workflows/coverage.yaml | 81 +-------------------------------- codecov.yml | 6 ++- 2 files changed, 7 insertions(+), 80 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index e367df892..bc60fff1a 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -33,8 +33,8 @@ jobs: - uses: ./.github/actions/cooldown-check coverage: - # Fork PRs get a read-only GITHUB_TOKEN, which would fail the PR coverage - # summary comment step. + # Fork PRs get no repo secrets, so the Codecov uploads (which run with + # fail_ci_if_error) would fail without CODECOV_TOKEN. if: ${{ github.event_name == 'push' || !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-24.04 # Backstop against hung compiles (the tester drives a pipeline still in @@ -43,7 +43,6 @@ jobs: timeout-minutes: 180 permissions: contents: read - pull-requests: write packages: read container: image: ghcr.io/nomicfoundation/solx-ci-runner@sha256:cd5a37f2630fdf1898ddb2ca11f8c2ecc4d572c60011fe923b27b1625f92ba15 @@ -218,82 +217,6 @@ jobs: -instr-profile="${PROFDATA_FILE}" \ "${OBJECTS[@]}" "${FORK_PATHS[@]}" > "./llvm/${LCOV_FILE_LLVM}" - - name: Coverage summary - id: coverage-summary - run: | - OBJECTS=( - -object "${GITHUB_WORKSPACE}/solx-instrumented" - -object "${GITHUB_WORKSPACE}/solx-tester-instrumented" - -object "${GITHUB_WORKSPACE}/solx-dev-instrumented" - ) - llvm-cov report -instr-profile=${PROFDATA_FILE} "${OBJECTS[@]}" \ - --ignore-filename-regex="${COVERAGE_IGNORE_REGEX}" \ - > coverage-report.txt - - # Parse llvm-cov report into per-crate markdown table - awk ' - /^---/ || /^Filename/ || /^$/ { next } - /^TOTAL/ { - total_lines = $(NF-5); total_missed = $(NF-4) - total_funcs = $(NF-8); total_missed_funcs = $(NF-7) - next - } - NF > 3 { - file = $1 - n = split(file, parts, "/") - crate = "" - for (i = 1; i <= n-1; i++) { - if (parts[i] ~ /^solx/ && parts[i+1] == "src") { - crate = parts[i]; break - } - } - if (crate == "") next - lines[crate] += $(NF-5) - missed[crate] += $(NF-4) - funcs[crate] += $(NF-8) - missed_f[crate] += $(NF-7) - } - END { - for (c in lines) { - lp = (lines[c] > 0) ? (lines[c] - missed[c]) / lines[c] * 100 : 0 - fp = (funcs[c] > 0) ? (funcs[c] - missed_f[c]) / funcs[c] * 100 : 0 - le = (lp >= 80) ? "🟢" : (lp >= 50) ? "🟡" : "🔴" - fe = (fp >= 80) ? "🟢" : (fp >= 50) ? "🟡" : "🔴" - printf "| %s | %s %.1f%% | %s %.1f%% |\n", c, le, lp, fe, fp - } - lp = (total_lines > 0) ? (total_lines - total_missed) / total_lines * 100 : 0 - fp = (total_funcs > 0) ? (total_funcs - total_missed_funcs) / total_funcs * 100 : 0 - le = (lp >= 80) ? "🟢" : (lp >= 50) ? "🟡" : "🔴" - fe = (fp >= 80) ? "🟢" : (fp >= 50) ? "🟡" : "🔴" - printf "ZTOTAL| **Total** | **%s %.1f%%** | **%s %.1f%%** |\n", le, lp, fe, fp - }' coverage-report.txt > coverage-rows.txt - - { - echo "| Crate | Line Coverage | Function Coverage |" - echo "|:------|:------------:|:-----------------:|" - grep -v '^ZTOTAL' coverage-rows.txt | sort - sed -n 's/^ZTOTAL//p' coverage-rows.txt - } > coverage-summary.md - - cat coverage-summary.md >> "${GITHUB_STEP_SUMMARY}" - { - echo "summary<> "${GITHUB_OUTPUT}" - - - name: Post PR coverage summary - if: github.event_name == 'pull_request' - uses: mshick/add-pr-comment@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12 - with: - message-id: 'coverage-summary' - message: | - ### Coverage Summary - - ${{ steps.coverage-summary.outputs.summary }} - - [Codecov Report](https://app.codecov.io/gh/${{ github.repository }}/pull/${{ github.event.pull_request.number }}) | [HTML Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts) | [Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - - name: Upload coverage artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/codecov.yml b/codecov.yml index 4799d1b62..183ef5eb3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,7 +8,11 @@ coverage: patch: default: false # disable the patch status that measures patch changes -comment: false +# The Codecov app's PR comment. `components` adds the per-area solx-llvm +# fork table (see component_management below); `flags` splits solx vs +# llvm-fork. +comment: + layout: "header, diff, flags, components, files" # solx-llvm fork coverage (uploaded under the `llvm-fork` flag) split by the # areas the LLVM team cares about. Paths are as they appear in the lcov from